main
sonorry 1 month ago
commit f1773af0ce

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/pick_student/pick_student.sqlite3" charset="UTF-8" />
</component>
</project>

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="test.fre.*" />
</list>
</option>
</inspection_tool>
</profile>
</component>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="rp" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pick_student.iml" filepath="$PROJECT_DIR$/.idea/pick_student.iml" />
</modules>
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,65 @@
# locustfile.py
import random
import string
from locust import HttpUser, TaskSet, task, between
class UserBehavior(TaskSet):
def on_start(self):
"""在测试开始时执行,进行登录并存储 token 和 username"""
# 假设有一个登录端点,可以获取 token 和 username
response = self.client.post("http://127.0.0.1:8000/teacher/jwt/token", data={
"username": "starhun",
"password": "123456"
})
if response.status_code == 200:
self.token = response.json()["access_token"]
self.username = response.json().get("username")
else:
self.token = None
self.username = None
@task(1)
def get_classes(self):
"""测试获取班级列表"""
if self.token:
with self.client.get("http://127.0.0.1:8000/teacher/pick_student/getclasses", headers={"Authorization": f"Bearer {self.token}"}) as response:
if response.status_code != 200:
response.failure(f"Failed to get classes: {response.text}")
@task(2)
def create_class(self):
characters = string.ascii_letters + string.digits
# 使用 random.choice() 从字符集合中随机选择字符
random_string = ''.join(random.choice(characters) for i in range(10))
"""测试创建班级"""
if self.token:
with self.client.post("http://127.0.0.1:8000/teacher/pick_student/create_class", params={"class_name": random_string,"class_time":"周三 5-6节"}, headers={"Authorization": f"Bearer {self.token}"}, catch_response=True) as response:
if response.status_code != 200:
response.failure(f"Failed to create class: {response.text}")
@task(3)
def random_pick(self):
"""测试随机点名"""
if self.token:
params = {"class_name": "aa"}
with self.client.get("http://127.0.0.1:8000/teacher/pick_student/random_pick/", params=params, headers={"Authorization": f"Bearer {self.token}"}, catch_response=True) as response:
if response.status_code != 200:
response.failure(f"Failed to random pick: {response.text}")
@task(4)
def upload_students(self):
"""测试上传学生信息"""
if self.token:
files = {
"file": ("stduents.xlsx", open("D:\python\code\pick_student\stduents.xlsx", "rb"), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
}
data = {"class_name": "班级1"}
with self.client.post("http://127.0.0.1:8000/teacher/pick_student/upload_students/", files=files, params=data, headers={"Authorization": f"Bearer {self.token}"}, catch_response=True) as response:
if response.status_code != 200:
response.failure(f"Failed to upload students: {response.text}")
class WebsiteUser(HttpUser):
tasks = [UserBehavior]
wait_time = between(1, 5)

@ -0,0 +1,2 @@
from .main import app1
from .login import app2

@ -0,0 +1,68 @@
from sqlalchemy.orm import Session
from pick_student import models,schemas
from pick_student.schemas import read_class
def get_student_by_id(db: Session, stduent_id: int,class_name: str,user_name: str):
return db.query(models.Student).filter(models.Student.student_id == stduent_id).filter(models.Student.class_name == class_name).filter(models.Student.user_name == user_name).first()
def get_student_by_name(db: Session, name: str):
return db.query(models.Student).filter(models.Student.name == name).first()
def get_students(db: Session, class_name: str, user_name: str,skip: int = 0, limit: int = 100):
return db.query(models.Student).filter(models.Student.class_name == class_name).filter(models.Student.user_name == user_name).offset(skip).limit(limit).all()
def create_student(db: Session, student: schemas.create_student):
db_student = models.Student(**student.dict())
db.add(db_student)
db.commit()
db.refresh(db_student)
return db_student
def update_student(db: Session, student: schemas.update_student,class_name: str, user_name: str):
db_student = get_student_by_id(db, student.student_id, class_name,user_name)
db_student.scores = student.scores
db_student.consecutive_calls=student.consecutive_calls
db_student.is_master=student.is_master
db_student.updated_at=student.updated_at
db_student.master_uses=student.master_uses
db.commit()
db.refresh(db_student)
return db_student
def delete_all_students(db: Session,class_name: str,user_name: str):
db.query(models.Student).filter(models.Student.class_name == class_name).filter(models.Student.user_name == user_name).delete()
db.commit()
def get_class_by_id(db: Session, class_id: int):
return db.query(models.Class).filter(models.Class.id == class_id).first()
def create_class(db: Session,cclass: schemas.create_class):
db_class = models.Class(**cclass.dict())
db.add(db_class)
db.commit()
db.refresh(db_class)
return db_class
def get_class_by_name(db: Session, class_name: str,user_name: str):
return db.query(models.Class).filter(models.Class.class_name == class_name).filter(models.Class.user_name == user_name).first()
def get_user(db: Session, username:str):
user= db.query(models.Teacher).filter(models.Teacher.user_name == username).first()
return user
def create_teacher(db: Session, user_name: str, password_hash: str):
db_teacher = models.Teacher(user_name=user_name, password_hash=password_hash)
db.add(db_teacher)
db.commit()
db.refresh(db_teacher)
return db_teacher
def get_classes_by_teacher_username(db: Session, user_name: str):
db_classes = db.query(models.Class).filter(models.Class.user_name == user_name).all()
return db_classes
def delete_class(db: Session, class_name: str, user_name: str):
db.query(models.Student).filter(models.Student.class_name == class_name).filter(models.Student.user_name == user_name).delete()
db.commit()
deleted_rows=db.query(models.Class).filter(models.Class.class_name == class_name).filter(models.Class.user_name == user_name).delete()
print(f"Deleted rows: {deleted_rows}")
db.commit()

@ -0,0 +1,18 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker,declarative_base
SQLALCHEMY_DATABASE_URL = 'sqlite:///./pick_student.sqlite3'
engine = create_engine(
SQLALCHEMY_DATABASE_URL, echo=True,connect_args={'check_same_thread': False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine,expire_on_commit=True)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
except Exception as e:
db.close()
raise e # 重新抛出异常
finally:
db.close()

@ -0,0 +1,94 @@
import secrets
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from jose import jwt, JWTError
from passlib.context import CryptContext
from pydantic import BaseModel
from sqlalchemy.orm import Session
from pick_student.database import SessionLocal, get_db
from pick_student.crud import get_user, create_teacher
app2=APIRouter()
SECRET_KEY = secrets.token_urlsafe()
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
class Token(BaseModel):
access_token: str
token_type: str
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def verify_password(plain_password: str, hashed_password:str):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str):
return pwd_context.hash(password)
def jwt_authenticate_user(db: Session, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user.password_hash):
return False
return user
def create_access_token(data: dict, expires_delta=None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app2.post("/jwt/token", response_model=Token)
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = jwt_authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"})
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.user_name}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app2.post("/jwt/register", response_model=Token)
def register_user(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
# 1. 检查用户名是否已经存在
existing_user = get_user(db, form_data.username)
if existing_user:
raise HTTPException(status_code=400, detail="Username already registered")
# 2. 对密码进行哈希处理
hashed_password = get_password_hash(form_data.password)
# 3. 创建新的用户并存储到数据库
user = create_teacher(db=db, user_name=form_data.username, password_hash=hashed_password)
# 4. 注册成功后生成 access_token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.user_name}, expires_delta=access_token_expires
)
# 5. 返回 access_token 和 token_type
return {"access_token": access_token, "token_type": "bearer","user_name": user.user_name}
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/teacher/jwt/token")
def get_current_teacher(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(db, username=username)
if user is None:
raise credentials_exception
return user

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
from typing import List, Optional
from cachetools import TTLCache
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Query, APIRouter
import pandas as pd
import io
import uvicorn
from sqlalchemy.orm import Session
from pick_student import crud, models
from pick_student.database import SessionLocal, Base, engine, get_db
from pick_student import schemas
from pick_student.login import get_current_teacher
from pick_student.progress_function import call_student, weighted_random_selection
app1 = APIRouter()
flag_cache = TTLCache(maxsize=1, ttl=86400)
flag_cache["is_reverse_day"] = False
Base.metadata.create_all(bind=engine)
@app1.get("/")
def read_root():
return {"message": "Welcome to the Student Management System!"}
@app1.post("/create_class", description="创建一个新班级")
def create_classroom(class_time:str,class_name: str, db: Session = Depends(get_db),
current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
# 检查班级是否已经存在
existing_class = crud.get_class_by_name(db, class_name,current_teacher.user_name)
if existing_class:
raise HTTPException(status_code=400, detail="班级名称已存在")
# 创建班级
cclass = schemas.create_class(class_name=class_name, user_name=current_teacher.user_name, class_time=class_time)
crud.create_class(db, cclass)
return {"message": "班级创建成功"}
@app1.post("/upload_students/",description="上传学生信息包括姓名和学号文件格式为Excel。")
async def upload_students(class_name: str,file: UploadFile = File(...),db: Session=Depends(get_db),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
raise HTTPException(status_code=400, detail="Invalid file type. Please upload an Excel file.")
# stdudents=crud.get_students(db, class_name)
# if stdudents:
# raise HTTPException(status_code=400, detail="Student list already exists")
# 读取上传的 Excel 文件
content = await file.read()
df_new = pd.read_excel(io.BytesIO(content))
df_new['class_name']= class_name
df_new['user_name']=current_teacher.user_name
# 确保包含所需列
if not {'name', 'student_id'}.issubset(df_new.columns):
raise HTTPException(status_code=400, detail="Excel file must contain 'name' and 'student_id' columns.")
for _, row in df_new.iterrows():
student_data = schemas.create_student(**row.to_dict())
crud.create_student(db,student_data)
# 更新全局 df
return {"message": "Student list uploaded successfully."}
@app1.get("/students/{id}",response_model=schemas.read_student,description="根据id获取学生信息")
def get_student(id: int,class_name: str = Query(..., description="班级名称", example="1班"), db: Session=Depends(get_db),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
db_student = crud.get_student_by_id(db, id, class_name,user_name=current_teacher.user_name)
if db_student is None:
raise HTTPException(status_code=404, detail="Student not found")
db_student.updated_at = db_student.updated_at.strftime('%Y-%m-%d %H:%M:%S')
return db_student
@app1.get("/get_students/",response_model=List[schemas.read_student],description="获取学生列表默认返回前100名学生")
def get_students(class_name: str = Query(..., description="班级名称", example="1班"),skip: int = 0, limit: int = 100, db: Session=Depends(get_db),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
students = crud.get_students(db, class_name,skip=skip, limit=limit,user_name=current_teacher.user_name)
if not students:
raise HTTPException(status_code=404, detail="No students found or class not exit")
for student in students:
student.updated_at = student.updated_at.strftime('%Y-%m-%d %H:%M:%S')
return students
@app1.put("/is_reverse_day/",description="是否是反转日(低分学生被点到概率翻倍回答问题得分1.5倍)")
def is_reverse_day(flag:Optional[bool]=False):
"""判断今天是否是反转日"""
# 这里可以根据日期或其他条件决定反转日
# 例如每周四为反转日,或者根据某个条件触发
# return datetime.today().weekday() == 3 # 每周四是反转日
flag_cache["is_reverse_day"] = flag
return {"message": f"Flag has been set {flag_cache['is_reverse_day']}"}
@app1.get("/random_pick/",description="从对应的班级随机选取一名学生,分数越高概率越低")
def random_pick(class_name: str = Query(..., description="班级名称", example="1班"),db: Session = Depends(get_db),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
# 获取学生列表
students = crud.get_students(db,class_name=class_name,user_name=current_teacher.user_name,skip=0,limit=1000)
if not students:
raise HTTPException(status_code=404, detail="No students found or class not exit")
# 随机选择学生
selected_student = weighted_random_selection(students)
return {"name": selected_student.name, "student_id": selected_student.student_id,"scores": selected_student.scores}
@app1.put("/update_score",description="更新学生的积分信息,依据到达课堂、问题回答的正确性和是否重复回答进行调整。在学生的数据库表中添加一个字段 consecutive_calls 记录学生连续被点名的次数。当某个学生连续三次被点名并且都答对了(分数大于2),就会进入“知识大师”状态,降低未来三次被点名的概率,如果被点到增加1分的得分。如果今天是反转日分数低的学生双倍概率被抽到并得到1.5倍的回答分数")
def update_score(class_name: str = Query(..., description="班级名称", example="1班"),
student_id: int = Query(..., description="学生的学号", example=102201415),
arrival: bool = Query(..., description="学生是否到达课堂", example=True),
question_repeated: bool = Query(..., description="问题是否重复回答", example=False),
question_correct: float = Query(..., description="问题回答的正确性评分介于0.5到3之间", ge=0.5, le=3, example=2.5),
db: Session = Depends(get_db),
current_teacher: schemas.create_teacher = Depends(get_current_teacher)
):
db_student = crud.get_student_by_id(db, student_id, class_name,user_name=current_teacher.user_name)
if db_student is None:
raise HTTPException(status_code=404, detail="Student not found")
# 更新到达课堂积分
if arrival:
db_student.scores += 1
# 更新回答问题积分
if question_repeated:
db_student.scores += 0.5
else:
db_student.scores -= 1
if question_correct>3.0 or question_correct<0.5:
raise HTTPException(status_code=412, detail="请输入0.5到3之间的数据")
else:
if db_student.is_master:
db_student.scores += 1
if flag_cache["is_reverse_day"]:
question_correct*=1.5
db_student.scores += question_correct
db_student=call_student(db_student,question_correct>2)
# 根据回答正确性加分
crud.update_student(db, db_student,class_name,current_teacher.user_name)
return {"message": "Score updated", "scores": db_student.scores,"consecutive_calls":db_student.consecutive_calls, "is_master": db_student.is_master,"master_uses":db_student.master_uses,"is_reverse_day":flag_cache["is_reverse_day"]}
@app1.get("/getclasses",response_model=List[schemas.read_class],description="根据教师用户名获取全部班级")
def get_classes(db: Session = Depends(get_db),user: schemas.create_teacher= Depends(get_current_teacher)):
classes = crud.get_classes_by_teacher_username(db, user.user_name)
if not classes:
raise HTTPException(status_code=404, detail="No classes found,please create class")
for class_ in classes:
class_.updated_at = class_.updated_at.strftime('%Y-%m-%d %H:%M:%S')
return classes
@app1.delete("/delete_students/", description="删除所有学生的记录")
def delete_all_students(db: Session = Depends(get_db),class_name: str = Query(..., description="班级名称", example="1班"),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
# 调用 crud 删除所有学生函数
crud.delete_all_students(db, class_name,current_teacher.user_name)
return {"message": f"All students in {class_name} have been deleted."}
@app1.delete("/delete_class/", description="删除班级")
def delete_class(class_name: str = Query(..., description="班级名称", example="1班"),db: Session = Depends(get_db),current_teacher: schemas.create_teacher = Depends(get_current_teacher)):
# 调用 crud 删除所有学生函数
crud.delete_class(db, class_name,current_teacher.user_name)
return {"message": f"Class {class_name} has been deleted."}
# if __name__ == "__main__":
# uvicorn.run(app, host="127.0.0.1", port=8000)

@ -0,0 +1,43 @@
from sqlalchemy import func
from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import Column, ForeignKey
from sqlalchemy.sql.sqltypes import Integer, String, FLOAT, DateTime, Boolean
from pick_student.database import Base, engine
class Student(Base): # 类名使用大写
__tablename__ = "student"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
student_id = Column(Integer, nullable=False, comment='学号')
name = Column(String, nullable=False, comment='姓名')
scores = Column(FLOAT, comment='得分', default=0)
consecutive_calls = Column(Integer, default=0) # 记录连续被点名的次数
is_master = Column(Boolean, default=False) # 是否进入知识大师状态
master_uses = Column(Integer, default=0) # 知识大师状态下的剩余次数
class_name = Column(String, ForeignKey('class.class_name'), nullable=False)
user_name = Column(String, ForeignKey('teacher.user_name'), nullable=False)# 外键引用修正
class_ = relationship("Class", back_populates="students")
created_at = Column(DateTime, server_default=func.now(), comment='创建时间')
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment='更新时间')
__mapped_args__ = {"order_by": id}
class Teacher(Base):
__tablename__ = "teacher"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_name = Column(String, nullable=False, comment='教师用户名')
password_hash = Column(String, nullable=False, comment='密码')
created_at = Column(DateTime, server_default=func.now(), comment='创建时间')
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment='更新时间')
classes = relationship("Class", back_populates="teacher")
# 班级表
class Class(Base):
__tablename__ = "class"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
class_name = Column(String, nullable=False, comment='班级名称')
class_time = Column(String, nullable=False, comment='开班时间')
user_name = Column(String, ForeignKey('teacher.user_name'), nullable=False)
teacher = relationship("Teacher", back_populates="classes")
students = relationship("Student", back_populates="class_") # 使用 class_ 避免与类名冲突
created_at = Column(DateTime, server_default=func.now(), comment='创建时间')
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment='更新时间')

@ -0,0 +1,40 @@
import random
def call_student(student, correct: bool):
if student.is_master:
student.master_uses -= 1
if student.master_uses <= 0:
student.is_master = False # 退出知识大师状态
if correct:
student.consecutive_calls += 1
if student.consecutive_calls >= 3:
student.consecutive_calls = 0
student.is_master = True
student.master_uses = 3 # 未来三次被点名概率降低
else:
student.consecutive_calls = 0
return student
def weighted_random_selection(students,reverse=False):
total_score = sum([student.scores for student in students])
# 计算每个学生的权重
probabilities = []
for student in students:
if student.is_master and student.master_uses > 0:
# 如果是知识大师,概率大幅降低
weight = (total_score - student.scores + 1) / (total_score + 1) * 0.1
elif reverse and student.scores <= 5:
# 反转机制:低分学生被点名概率增加
weight = (total_score - student.scores + 1) / (total_score + 1) * 2 # 概率翻倍
else:
# 正常情况下的加权选择,分数越高概率越低
weight = (total_score - student.scores + 1) / (total_score + 1)
probabilities.append(weight)
# 使用随机加权选择
selected = random.choices(students, weights=probabilities, k=1)[0]
return selected

@ -0,0 +1,45 @@
from datetime import datetime
from pydantic import BaseModel
class create_student(BaseModel):
student_id: int
name: str
class_name: str
user_name: str
class update_student(create_student):
student_id : int
name: str
class_name: str
scores: float
consecutive_calls: int
master_uses: int
is_master: bool
class read_student(create_student):
id: int
student_id: int
name: str
scores: float
updated_at: str
consecutive_calls: int
is_master:bool
master_uses: int
class Config:
from_attributes = True
class create_class(BaseModel):
user_name: str
class_name: str
class_time: str
class Config:
from_attributes = True # 使模型与 SQLAlchemy ORM 模型兼容
class create_teacher(BaseModel):
user_name: str
password: str
class read_class(BaseModel):
id: int
class_name: str
updated_at: str
class_time: str
class Config:
from_attributes = True

@ -0,0 +1,22 @@
from fastapi import FastAPI
import uvicorn
from starlette.middleware.cors import CORSMiddleware
from pick_student import app1,app2
app=FastAPI(
title="学生点名系统",
description="学生点名系统",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"], # 允许所有方法
allow_headers=["*"],
)
app.include_router(app1, prefix="/teacher/pick_student",tags=["学生点名系统"])
app.include_router(app2, prefix="/teacher",tags=["教师登录系统"])
if __name__=='__main__':
uvicorn.run('run:app',host='127.0.0.1',port=8000,reload=True)

@ -0,0 +1,86 @@
import unittest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pick_student.database import Base
from pick_student.login import get_password_hash
from pick_student.main import get_db
from run import app # 从 run.py 导入主 app
from pick_student.crud import create_teacher
# 设置测试数据库
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_test.db" # 使用不同的数据库文件名
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 创建所有表
Base.metadata.create_all(bind=engine)
# 重写 get_db 函数用于测试
def override_get_db():
db = TestingSessionLocal()
try:
yield db
except Exception as e:
db.close()
raise e # 重新抛出异常
finally:
db.close() # 确保数据库连接被正确关闭
# 覆盖 app 中的依赖项
app.dependency_overrides[get_db] = override_get_db
# 创建测试客户端
client = TestClient(app)
class TestAuthRoutes(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 在测试开始时设置初始数据库状态
db = TestingSessionLocal()
hashed_password = get_password_hash("password") # 假设已经生成一个哈希密码
create_teacher(db, user_name="test_teacher", password_hash=hashed_password)
db.close()
def test_login_success(self):
# 测试登录成功的情况
response = client.post(
"/teacher/jwt/token",
data={"username": "test_teacher", "password": "password"},
)
print(response.json())
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("access_token", data)
self.assertIn("token_type", data)
def test_login_failure(self):
# 测试登录失败的情况
response = client.post(
"/teacher/jwt/token",
data={"username": "wrong_teacher", "password": "wrong_password"},
)
self.assertEqual(response.status_code, 401)
data = response.json()
self.assertEqual(data["detail"], "Incorrect username or password")
def test_register_user(self):
# 测试注册新用户
response = client.post(
"/teacher/jwt/register",
data={"username": "new_teacher", "password": "new_password"},
)
print(response.json())
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("access_token", data)
self.assertIn("token_type", data)
@classmethod
def tearDownClass(cls):
# 清理测试数据库
Base.metadata.drop_all(bind=engine)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,153 @@
import io
import unittest
import pandas as pd
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pick_student import crud, schemas
from pick_student.database import Base
from pick_student.login import get_password_hash
from pick_student.main import get_db
from run import app # 从 run.py 导入主 app
from pick_student.crud import create_teacher, create_class
# 设置测试数据库
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_test.db" # 使用不同的数据库文件名
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 创建所有表
Base.metadata.create_all(bind=engine)
# 重写 get_db 函数用于测试
def override_get_db():
db = TestingSessionLocal()
try:
yield db
except Exception as e:
db.close()
raise e # 重新抛出异常
finally:
db.close() # 确保数据库连接被正确关闭
# 覆盖 app 中的依赖项
app.dependency_overrides[get_db] = override_get_db
# 创建测试客户端
client = TestClient(app)
class TestMainRoutes(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 在测试开始时设置初始数据库状态
db = TestingSessionLocal()
hashed_password = get_password_hash("password") # 假设已经生成一个哈希密码
create_teacher(db, user_name="test_teacher", password_hash=hashed_password)
crud.create_class(db, cclass=schemas.create_class(class_name="2班", user_name="test_teacher",class_time="周三 5-6节"))
crud.create_class(db, cclass=schemas.create_class(class_name="3班", user_name="test_teacher",class_time="周三 5-6节"))
crud.create_student(db, student=schemas.create_student(class_name="2班", name="Alice", student_id=101, user_name="test_teacher"))
crud.create_student(db, student=schemas.create_student(class_name="2班", name="Bob", student_id=102,user_name="test_teacher"))
crud.create_student(db, student=schemas.create_student(class_name="2班", name="Charlie", student_id=103, user_name="test_teacher"))
crud.create_student(db, student=schemas.create_student(class_name="3班", name="David", student_id=104, user_name="test_teacher"))
db.close()
def test_create_classroom(self):
# 测试创建班级的成功情况
response = client.post(
"/teacher/pick_student/create_class/",
params={"class_name": "1班","class_time":"周三 5-6节"},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["message"], "班级创建成功")
def test_upload_students(self):
# 测试上传学生信息的成功情况
class_name = "1班"
data = {
'name': ['Alice', 'Bob'],
'student_id': [101, 102]
, 'user_name': ['test_teacher', 'test_teacher']
}
df = pd.DataFrame(data)
excel_buffer = io.BytesIO()
df.to_excel(excel_buffer, index=False)
excel_buffer.seek(0)
response = client.post(
"/teacher/pick_student/upload_students/",
files={"file": ("students.xlsx", excel_buffer, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
params={"class_name": class_name},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["message"], "Student list uploaded successfully.")
def test_get_students(self):
response = client.get(
"/teacher/pick_student/get_students/",
params={"class_name": "2班"},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
self.assertEqual(response.status_code, 200)
def test_update_score(self):
response = client.put(
"/teacher/pick_student/update_score/",
params={"class_name": "2班", "student_id": 101, "arrival": True, "question_repeated": False, "question_correct": 2.5},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
print(response.json())
self.assertEqual(response.status_code, 200)
self.assertIn("scores", response.json())
def test_is_reverse_day(self):
response = client.put(
"/teacher/pick_student/is_reverse_day/",
params={"flag": True},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
print(response.json())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"message": "Flag has been set True"})
def test_random_pick(self):
# 测试随机点名的功能
response = client.get(
"/teacher/pick_student/random_pick/",
params={"class_name": "2班"},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
print(response.json())
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("name", data)
self.assertIn("student_id", data)
def test_get_classes(self):
response = client.get(
"/teacher/pick_student/getclasses/",
headers={"Authorization": "Bearer " + self.get_access_token()}
)
self.assertEqual(response.status_code, 200)
data = response.json()
def test_delete_class(self):
response = client.delete(
"/teacher/pick_student/delete_class/",
params={"class_name": "3班"},
headers={"Authorization": "Bearer " + self.get_access_token()}
)
self.assertEqual(response.status_code, 200)
def get_access_token(self):
# 获取访问令牌
response = client.post(
"/teacher/jwt/token",
data={"username": "test_teacher", "password": "password"},
)
return response.json()["access_token"]
@classmethod
def tearDownClass(cls):
# 清理测试数据库
Base.metadata.drop_all(bind=engine)
if __name__ == "__main__":
unittest.main()

Binary file not shown.

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>萝卜册登录</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
}
},
"exclude": ["node_modules", "dist"]
}

@ -0,0 +1,483 @@
{
"name": "roll-book",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roll-book",
"version": "0.0.0",
"dependencies": {
"vue": "^3.5.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.8"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz",
"integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz",
"integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz",
"integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.25.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz",
"integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.7",
"@babel/helper-validator-identifier": "^7.25.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@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/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
"integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz",
"integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz",
"integrity": "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.11",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz",
"integrity": "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.11",
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz",
"integrity": "sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.11",
"@vue/compiler-dom": "3.5.11",
"@vue/compiler-ssr": "3.5.11",
"@vue/shared": "3.5.11",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.47",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz",
"integrity": "sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.11",
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.11.tgz",
"integrity": "sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.11.tgz",
"integrity": "sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.11",
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.11.tgz",
"integrity": "sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.11",
"@vue/runtime-core": "3.5.11",
"@vue/shared": "3.5.11",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.11.tgz",
"integrity": "sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.11",
"@vue/shared": "3.5.11"
},
"peerDependencies": {
"vue": "3.5.11"
}
},
"node_modules/@vue/shared": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz",
"integrity": "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/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.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"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/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"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.7",
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
"integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.24.0",
"@rollup/rollup-android-arm64": "4.24.0",
"@rollup/rollup-darwin-arm64": "4.24.0",
"@rollup/rollup-darwin-x64": "4.24.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
"@rollup/rollup-linux-arm-musleabihf": "4.24.0",
"@rollup/rollup-linux-arm64-gnu": "4.24.0",
"@rollup/rollup-linux-arm64-musl": "4.24.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
"@rollup/rollup-linux-riscv64-gnu": "4.24.0",
"@rollup/rollup-linux-s390x-gnu": "4.24.0",
"@rollup/rollup-linux-x64-gnu": "4.24.0",
"@rollup/rollup-linux-x64-musl": "4.24.0",
"@rollup/rollup-win32-arm64-msvc": "4.24.0",
"@rollup/rollup-win32-ia32-msvc": "4.24.0",
"@rollup/rollup-win32-x64-msvc": "4.24.0",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/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/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/vite": {
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"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.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.11.tgz",
"integrity": "sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.11",
"@vue/compiler-sfc": "3.5.11",
"@vue/runtime-dom": "3.5.11",
"@vue/server-renderer": "3.5.11",
"@vue/shared": "3.5.11"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
}
}

@ -0,0 +1,20 @@
{
"name": "roll-book",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"vue": "^3.5.10",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.8"
}
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,21 @@
<template>
<div id="app">
<router-view path=""></router-view>
</div>
</template>
<script>
export default {
}
</script>
<style>
/* Global styles */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@ -0,0 +1,96 @@
<template>
<div id="app">
<div class="register-container">
<img src="../assets/rabbit.png" alt="背景图片" class="background-image" />
<form @submit.prevent="register">
<h3>欢迎注册萝卜册</h3>
<div><input type="text" placeholder="昵称" v-model="nickname" /></div>
<div><input type="password" placeholder="密码" v-model="password" /></div>
<button type="submit">注册</button>
<p v-if="message">{{ message }}</p>
<p>已有账号? <a href="#">马上登录</a></p>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
nickname: '',
password: '',
message: ''
};
},
methods: {
async register() {
try {
const params = new URLSearchParams();
params.append('username', this.nickname);
params.append('password', this.password);
const response = await axios.post('http://127.0.0.1:8000/teacher/jwt/register', params);
this.message = '注册成功,正在跳转到登录页面...';
console.log(response.data);
//
setTimeout(() => {
window.location.href = 'login.html'; // login.html
}, 2000); // 2
} catch (error) {
// 使
if (error.response && error.response.data && error.response.data.detail) {
this.message = `注册失败: ${error.response.data.detail}`;
} else {
this.message = '注册失败,请重试';
}
console.error(error);
}
}
}
};
</script>
<style scoped>
.register-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 50px;
}
input {
margin: 10px 0;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #368b75;
}
a {
color: #42b983;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.background-image {
position: absolute;
left: 0;
top: 0;
width: 400px;
height: 1024px;
z-index: 1;
}
</style>

@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

@ -0,0 +1,45 @@
import { createRouter, createWebHistory } from 'vue-router'
import LoginPage from '../views/LoginPage.vue'
import RegisterPage from '../views/RegisterPage.vue'
import ClassManagement from '../views/ClassManagement.vue'
import CreateClass from '../views/CreateClass.vue'
import StudentManagement from '../views/StudentManagement.vue'
import RollBook from '../views/RollBook.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'ClassManagement',
component: ClassManagement
},
{
path: '/login',
name: 'Login',
component: LoginPage
},
{
path: '/register',
name: 'Register',
component: RegisterPage
},
{
path: '/create_class',
name: 'CreateClass',
component: CreateClass
},
{
path: '/student_management/:className',
name: 'StudentManagement',
component: StudentManagement
},
{
path: '/random_pick/:className',
name: 'RollBook',
component: RollBook
},
]
})
export default router

@ -0,0 +1,309 @@
<template>
<div class="class-management-page">
<div>
<div class="bgdiagram_1"></div>
<div class="bgdiagram_2"></div>
<div class="bgdiagram_3"></div>
<div class="bgdiagram_4"></div>
<div class="bgdiagram_5"></div>
<span class="NickName">{{nickname}}</span>
<router-link to="/login">
<span class="login_in" v-if="nickname ===''"></span>
</router-link>
<router-link to="/login">
<span class="quitLogin">退出</span>
</router-link>
<span class="classCount">班级数 1</span>
<router-link to="/create_class">
<button class="createClass">+新建班级</button>
</router-link>
</div>
<table class="class_manage">
<thead>
<tr>
<th>班级名</th>
<th>上课时间</th>
<th>动作</th>
</tr>
</thead>
<tbody v-if="paginatedClasses.length">
<tr v-for="cclass in paginatedClasses" :key="cclass.id">
<td>{{ cclass.class_name }}</td>
<td>{{ cclass.class_time }}</td>
<td>
<button class="button_style" @click="goToStudentManagement(cclass.class_name)"></button>
<button class="button_style" @click="manageClass(cclass.class_name)"></button>
<button class="button_style" @click="deleteClass(cclass.class_name)"></button>
</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default{
data() {
return {
nickname: '',
className: '',
classes: [],
classTime: '',
errorMessage: '',
currentPage: 1, //
pageSize: 5, //
}
},
computed: {
// className() {
// return this.$route.params.className;
// },
totalPages() {
return Math.ceil(this.classes.length / this.pageSize); //
},
paginatedClasses() {
const start = (this.currentPage - 1) * this.pageSize; //
return this.classes.slice(start, start + this.pageSize); //
},
},
methods: {
async fetchClasses() {
try {
const response = await axios.get('http://127.0.0.1:8000/teacher/pick_student/getclasses', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.classes = response.data;
} catch (error) {
console.error('获取班级信息失败', error);
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
},
//
async deleteClass(className) {
try {
const response = await axios.delete(`http://127.0.0.1:8000/teacher/pick_student/delete_class/?class_name=${className}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
});
alert(response.data.message); //
this.fetchClasses(); //
} catch (error) {
console.error('删除班级失败', error);
alert('删除班级失败,请稍后重试。');
}
},
manageClass(className) {
// 使
this.$router.push({ name: 'RollBook', params: { className } });
},
goToStudentManagement(className) {
//
this.$router.push({ name: 'StudentManagement', params: { className } });
}
},
created() {
this.fetchClasses(); //
this.nickname = localStorage.getItem('nickname'); //
}
}
</script>
<style scoped>
.bgdiagram_1 {
width: 100%;
height: 300px;
background: rgba(94,162,218,1);
opacity: 1;
position: absolute;
top: 0px;
left: 0px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
overflow: hidden;
}
.bgdiagram_2 {
width: 197px;
height: 197px;
background: rgba(217,217,217,0.20000000298023224);
opacity: 1;
position: absolute;
top: 77px;
left: 57px;
border-radius: 50%;
}
.bgdiagram_3 {
width: 266px;
height: 215px;
background: rgba(217,217,217,0.20000000298023224);
opacity: 1;
position: absolute;
top: 132px;
left: 666px;
border-radius: 50%;
}
.bgdiagram_4 {
width: 84px;
height: 84px;
background: linear-gradient(rgba(217,217,217,0.20000000298023224), rgba(115,115,115,0.20000000298023224));
opacity: 1;
position: absolute;
top: 36px;
left: 1045px;
border-radius: 50%;
}
.bgdiagram_5 {
width: 80%;
height: 60px;
background: rgba(217,217,217,0.4000000059604645);
opacity: 1;
position: absolute;
top: 240px;
left: 120px;
overflow: hidden;
}
.class_manage {
width: 80%;
height: 200px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 324px;
left: 120px;
border: 1px solid rgba(0,0,0,0.10000000149011612);
overflow: hidden;
}
td {
text-align: center;
}
.NickName {
width: 198px;
color: rgba(255,255,255,1);
position: absolute;
top: 130px;
left: 120px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.quitLogin {
width: 60px;
color: rgba(255,255,255,1);
position: absolute;
top: 180px;
left: 220px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.classCount {
width: 104px;
color: rgba(255,255,255,1);
position: absolute;
top: 259px;
left: 150px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.createClass {
width: 125px;
color: rgba(255,255,255,100);
background-color: rgba(255, 255, 255, 0);
border-width: 1px;
position: absolute;
top: 258px;
left: 318px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.button_style {
width: 90px;
height: 40px;
background: rgba(24,144,255,1);
opacity: 1;
color: white;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.login_in {
width: 80px;
color: rgba(255,255,255,1);
position: absolute;
top: 180px;
left: 120px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.pagination {
top: 850px;
position: relative;
z-index: 10;
text-align: center; /* 居中对齐 */
margin: 20px 0; /* 添加上下边距 */
}
.pagination button {
padding: 10px 20px; /* 添加内边距 */
margin: 0 10px; /* 添加左右边距 */
border: none;
border-radius: 5px; /* 添加圆角 */
background: rgba(24, 144, 255, 1);
color: white;
cursor: pointer;
}
.pagination button:disabled {
background: rgba(200, 200, 200, 0.5); /* 禁用状态颜色 */
cursor: not-allowed; /* 光标样式 */
}
</style>

@ -0,0 +1,223 @@
<template>
<div class="v32_21">
<div class="v32_22"></div>
<div class="v32_23"></div>
<div class="v32_37"></div>
<h2 class="v32_24">新建班级</h2>
<h4 class="v32_30">班级名称</h4>
<h4 class="v32_31">上课时间</h4>
<div @submit.prevent="createClass">
<input type="text" v-model="className" class="v32_25">
<input type="text" v-model="classTime" class="v32_26">
<button class="v32_28" @click="createClass"></button>
</div>
<p class="error-message" v-if="errorMessage">{{ errorMessage }}</p>
</div>
</template>
<script>
import axios from 'axios';
export default{
data() {
return {
className: '',
classTime: '',
errorMessage: ''
}
},
methods: {
async createClass() {
const username = localStorage.getItem('username');
this.errorMessage = ''; //
try {
await axios.post('http://127.0.0.1:8000/teacher/pick_student/create_class', null, {
params: {
user_name: username,
class_name: this.className,
class_time: this.classTime
},
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.className = ''; //
this.classTime = ''; //
setTimeout(() => {
window.location.href = '/'; // class_management
}, 100);
} catch (error) {
//
if (error.response && error.response.data) {
this.errorMessage = error.response.data.detail || '创建班级失败,请重试';
} else {
this.errorMessage = '网络错误,请重试';
}
console.error('创建班级失败', error);
}
},
},
}
</script>
<style scoped>
* {
box-sizing: border-box;
}
body {
font-size: 14px;
}
.v32_21 {
width: 100%;
height: 1024px;
background: rgba(250,250,250,1);
opacity: 1;
position: absolute;
top: 0px;
left: 0px;
overflow: hidden;
}
.v32_22 {
width: 80%;
height: 48px;
background: rgba(241,248,255,1);
opacity: 1;
position: absolute;
top: 300px;
left: 120px;
overflow: hidden;
}
.v32_23 {
width: 80%;
height: 500px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 348px;
left: 120px;
overflow: hidden;
}
.v32_28 {
width: 100px;
height: 48px;
background: rgba(24,144,255,1);
opacity: 1;
position: absolute;
top: 680px;
left: 160px;
border: 0px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
overflow: hidden;
color: white;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 20px;
text-align: center;
}
.v32_24 {
width: 400px;
color: rgba(0,0,0,1);
position: absolute;
top: 270px;
left: 500px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 36px;
opacity: 1;
text-align: center;
}
.v32_25 {
width: 400px;
height: 60px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 418px;
left: 160px;
border: 1px solid rgba(0,0,0,0.20000000298023224);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
overflow: hidden;
}
.v32_26 {
width: 700px;
height: 60px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 548px;
left: 160px;
border: 1px solid rgba(0,0,0,0.20000000298023224);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
overflow: hidden;
}
.v32_30 {
width: 200px;
color: rgba(0,0,0,1);
position: absolute;
top: 360px;
left: 160px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.v32_31 {
width: 200px;
color: rgba(0,0,0,1);
position: absolute;
top: 490px;
left: 160px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.v32_34 {
width: 100px;
color: rgba(255,255,255,1);
position: absolute;
top: 686px;
left: 160px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.v32_37 {
width: 100%;
height: 300px;
background: rgba(94,162,218,1);
opacity: 1;
position: absolute;
top: 0px;
left: 0px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
overflow: hidden;
}
.error-message {
color: red;
margin-top: 10px;
opacity: 1;
position: absolute;
top: 750px;
left: 160px;
}
</style>>

@ -0,0 +1,190 @@
<template>
<div class="login-page">
<div class="login-container">
<section class="image-section">
<img src="https://cdn.builder.io/api/v1/image/assets/TEMP/1e9af2397a249392b659cd96eab793a73bb5bebb447827998f2ceb1304997200?placeholderIfAbsent=true&apiKey=f28f4cadbac64bf48902dfcdb6485882" class="login-image" alt="Login illustration" />
</section>
<section class="form-section" @submit.prevent="login">
<form class="login-form">
<h1 class="welcome-text">欢迎登录萝卜册</h1>
<input type="text" id="nickname" class="input-nickname" placeholder="昵称" v-model="nickname"/>
<input type="password" id="password" class="input-password" placeholder="密码" v-model="password"/>
<button type="submit" class="login-button" @click="login"></button>
<p class="message-link" v-if="message">{{ message }}</p>
<router-link to="/register" class="register-link">没有账号注册一个</router-link>
</form>
</section>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
nickname: '',
password: '',
message: ''
};
},
methods: {
async login() {
try {
const params = new URLSearchParams();
params.append('username', this.nickname); // FastAPI 'username'
params.append('password', this.password);
const response = await axios.post('http://127.0.0.1:8000/teacher/jwt/token', params);
this.message = '登录成功';
console.log(response.data);
// access_token localStorage
localStorage.setItem('access_token', response.data.access_token);
// nickname localStorage
localStorage.setItem('nickname', this.nickname); //
//
setTimeout(() => {
window.location.href = '/'; // class_management.html
}, 100);
} catch (error) {
this.message = '用户名或密码错误';
console.error(error);
}
}
}
};
</script>
<style scoped>
.login-page {
background-color: #fff;
padding: 0 80px 0 0;
overflow: hidden;
}
.login-container {
display: flex;
gap: 20px;
}
.image-section {
flex: 1;
}
.login-image {
aspect-ratio: 0.39;
object-fit: contain;
width: 50%;
}
.form-section {
flex: 1;
}
.login-form {
display: flex;
flex-direction: column;
color: #000;
font: 400 24px Microsoft Yi Baiti, -apple-system, Roboto, Helvetica, sans-serif;
margin-top: 377px;
}
.welcome-text {
width: 412px;
color: rgba(0,0,0,1);
position: absolute;
top: 377px;
left: 514px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 48px;
opacity: 1;
text-align: left;
}
.input-nickname {
width: 412px;
height: 58px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 540px;
left: 514px;
border: 1px solid rgba(0,0,0,0.30000001192092896);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.input-password {
width: 412px;
height: 58px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 671px;
left: 514px;
border: 1px solid rgba(0,0,0,0.30000001192092896);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.login-button {
width: 412px;
height: 68px;
background: rgba(0,0,0,1);
opacity: 1;
position: absolute;
top: 791px;
left: 514px;
border-top-left-radius: 34px;
border-top-right-radius: 34px;
border-bottom-left-radius: 34px;
border-bottom-right-radius: 34px;
overflow: hidden;
color: rgba(255,255,255,1);
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.message-link {
width: 390px;
color: rgba(0,0,0,1);
position: absolute;
top: 900px;
left: 536px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.register-link {
width: 390px;
color: rgba(0,0,0,1);
position: absolute;
top: 872px;
left: 536px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 16px;
opacity: 1;
text-align: center;
}
</style>

@ -0,0 +1,189 @@
<template>
<div class="register-page">
<div class="register-container">
<section class="image-section">
<img src="https://cdn.builder.io/api/v1/image/assets/TEMP/1e9af2397a249392b659cd96eab793a73bb5bebb447827998f2ceb1304997200?placeholderIfAbsent=true&apiKey=f28f4cadbac64bf48902dfcdb6485882" class="register-image" alt="register illustration" />
</section>
<section class="form-section" @submit.prevent="register">
<form class="register-form">
<h1 class="welcome-text">欢迎注册萝卜册</h1>
<input type="text" id="nickname" class="input-nickname" placeholder="昵称" v-model="nickname"/>
<input type="password" id="password" class="input-password" placeholder="密码" v-model="password"/>
<button type="submit" class="register-button" @click="register"></button>
<p class="message-link" v-if="message">{{ message }}</p>
<router-link to="/login" class="login-link">已有账号马上登录</router-link>
</form>
</section>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
nickname: '',
password: '',
message: ''
};
},
methods: {
async register() {
try {
const params = new URLSearchParams();
params.append('username', this.nickname);
params.append('password', this.password);
const response = await axios.post('http://127.0.0.1:8000/teacher/jwt/register', params);
this.message = '注册成功,正在跳转到登录页面...';
console.log(response.data);
//
setTimeout(() => {
window.location.href = '/login'; // login.html
}, 200);
} catch (error) {
// 使
if (error.response && error.response.data && error.response.data.detail) {
this.message = `注册失败: ${error.response.data.detail}`;
} else {
this.message = '注册失败,请重试';
}
console.error(error);
}
}
}
};
</script>
<style scoped>
.register-page {
background-color: #fff;
padding: 0 80px 0 0;
overflow: hidden;
}
.register-container {
display: flex;
gap: 20px;
}
.image-section {
flex: 1;
}
.register-image {
aspect-ratio: 0.39;
object-fit: contain;
width: 50%;
}
.form-section {
flex: 1;
}
.register-form {
display: flex;
flex-direction: column;
color: #000;
font: 400 24px Microsoft Yi Baiti, -apple-system, Roboto, Helvetica, sans-serif;
margin-top: 377px;
}
.welcome-text {
width: 412px;
color: rgba(0,0,0,1);
position: absolute;
top: 377px;
left: 514px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 48px;
opacity: 1;
text-align: left;
}
.input-nickname {
width: 412px;
height: 58px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 540px;
left: 514px;
border: 1px solid rgba(0,0,0,0.30000001192092896);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.input-password {
width: 412px;
height: 58px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 671px;
left: 514px;
border: 1px solid rgba(0,0,0,0.30000001192092896);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.register-button {
width: 412px;
height: 68px;
background: rgba(0,0,0,1);
opacity: 1;
position: absolute;
top: 791px;
left: 514px;
border-top-left-radius: 34px;
border-top-right-radius: 34px;
border-bottom-left-radius: 34px;
border-bottom-right-radius: 34px;
overflow: hidden;
color: rgba(255,255,255,1);
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.message-link {
width: 390px;
color: rgba(0,0,0,1);
position: absolute;
top: 900px;
left: 536px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.login-link {
width: 390px;
color: rgba(0,0,0,1);
position: absolute;
top: 872px;
left: 536px;
font-family: Microsoft Yi Baiti;
font-weight: Regular;
font-size: 16px;
opacity: 1;
text-align: center;
}
</style>

@ -0,0 +1,630 @@
<template>
<div class="RollBook_page">
<span class="text_DigRadish">哪个萝卜被挖出来了</span>
<div class="rabbit_picture"></div>
<div v-if="selectedStudent && selectedStudent.is_master" class="show_master"></div>
<button class="randomPick" @click="randomPick"></button>
<div v-if="selectedStudent">
<div class="student">学生: {{ selectedStudent.name }}</div>
<label class="checkbox-wrapper position_arrival">
<input type="checkbox" v-model="arrival" class="custom-checkbox">
<span class="checkbox-label">已到</span>
</label>
<br>
<label class="checkbox-wrapper position_questionRepeated">
<input type="checkbox" v-model="questionRepeated" class="custom-checkbox">
<span class="checkbox-label">已复述</span>
</label>
<br>
<span class="answerScore">本轮答题积分</span>
<label class="position_answerScore">
<input type="number" v-model="questionCorrect" min="0.5" max="3" step="0.1"
style="width: 100%; height: 100%; border: none; padding: 0 10px; box-sizing: border-box;">
</label>
<span class="totalScore">总积分</span>
<label class=" position_Score">
{{selectedStudent.scores}}
</label>
<label class="checkbox-wrapper position_putScore">
<button @click="updateScore" class="checkbox-label">更新分数</button>
</label>
</div>
<div v-if="uploadSuccessMessage" class="success-message">{{ uploadSuccessMessage }}</div>
<div v-if="uploadErrorMessage" class="error-message">{{ uploadErrorMessage }}</div>
<div class="reverse-day-container">
<label for="reverse-day" >反转日</label>
<label class="switch">
<input type="checkbox" id="reverse-day" v-model="isReverseDay" @change="toggleReverseDay">
<span class="slider"></span>
</label>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default{
data() {
return {
selectedStudent: null,
arrival: false,
questionRepeated: false,
questionCorrect: 1.0,
uploadSuccessMessage: '',
uploadErrorMessage: '',
isReverseDay: false
}
},
computed: {
className() {
console.log('className computed property is called',this.$route.params.className);
return this.$route.params.className;
}
},
methods: {
async randomPick() {
try {
const response = await axios.get(`http://127.0.0.1:8000/teacher/pick_student/random_pick/?class_name=${this.className}`,{
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.selectedStudent = response.data;
} catch (error) {
console.error('随机点名失败', error);
alert(error.response?.data?.detail || '点名失败,请重试。');
}
},
async updateScore() {
if (!this.selectedStudent) {
alert('请先点名选择学生');
return;
}
try {
//
await axios.put(`http://127.0.0.1:8000/teacher/pick_student/update_score`, null, {
params: {
class_name: this.className,
student_id: this.selectedStudent.student_id,
arrival: this.arrival,
question_repeated: this.questionRepeated,
question_correct: this.questionCorrect
},
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.uploadSuccessMessage = '分数已更新';
this.uploadErrorMessage = '';
// get_student
await this.fetchStudentInfo(this.selectedStudent.student_id);
} catch (error) {
console.error('更新分数失败', error);
this.uploadErrorMessage = error.response?.data?.detail || '更新分数失败,请重试。';
this.uploadSuccessMessage = '';
}
},
async fetchStudentInfo(student_id) {
try {
const response = await axios.get(`http://127.0.0.1:8000/teacher/pick_student/students/${student_id}`, {
params: {
class_name: this.className
},
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.selectedStudent = response.data;
} catch (error) {
console.error('获取学生信息失败', error);
alert(error.response?.data?.detail || '获取学生信息失败,请重试。');
}
},
async toggleReverseDay() {
try {
const response = await axios.put(`http://127.0.0.1:8000/teacher/pick_student/is_reverse_day`, null, {
params: {
flag: this.isReverseDay
}
});
alert(response.data.message);
} catch (error) {
console.error('设置反转日失败', error);
alert(error.response?.data?.detail || '设置反转日失败,请重试。');
}
}
}
}
</script>
<style scoped>
.position_arrival {
top: 718px;
left: 1175px;
}
.position_questionRepeated {
top: 779px;
left: 1175px;
}
.position_answerScore {
width: 160px;
height: 48px;
background: rgba(179,185,187,1);
opacity: 1;
position: absolute;
top: 722px;
left: 919px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.position_Score {
width: 160px;
height: 38px;
background: rgba(179,185,187,1);
color: white;
opacity: 1;
position: absolute;
top: 722px;
left: 640px;
text-align: center;
padding: 10px;
font-size: 30px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.position_putScore {
top: 710px;
left: 919px;
}
.checkbox-wrapper {
display: inline-block;
width: 160px;
height: 68px;
position: relative;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.custom-checkbox {
display: none; /* 隐藏默认复选框 */
}
.checkbox-label {
display: inline-block;
width: 100%;
height: 100%;
color: rgba(255,255,255,1);
background: rgba(24,144,255,1);
text-align: center;
line-height: 68px; /* 设置行高使文字居中 */
font-family: Microsoft YaHei;
font-weight: Regular;
font-size: 32px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
cursor: pointer;
user-select: none;
}
/* 选中时的样式 */
.custom-checkbox:checked + .checkbox-label {
background: rgba(146,188,153,1);
}
.reverse-day-container {
position: absolute;
top: 10px;
right: 20px;
}
/* 滑动开关样式 */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #28a745;
}
input:checked + .slider:before {
transform: translateX(26px);
}
body {
font-size: 14px;
}
button {
width: 120px;
height: 68px;
background: rgba(24,144,255,1);
opacity: 1;
position: absolute;
border: 0px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.show_master {
width: 300px;
height: 500px;
top: 500px;
left: 200px;
background: url("../assets/knowledgemaster.png");
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
opacity: 1;
position: absolute;
overflow: hidden;
}
.RollBook_page {
width: 100%;
height: 1024px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 0px;
left: 0px;
overflow: hidden;
}
.text_DigRadish {
width: 252px;
color: rgba(0,0,0,1);
position: absolute;
top: 540px;
left: 610px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.rabbit_picture {
width: 459px;
height: 391px;
background: url("../assets/rabbit2.png");
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
opacity: 1;
position: absolute;
top: 55px;
left: 476px;
overflow: hidden;
}
.v2004_36 {
width: 120px;
height: 97px;
background: linear-gradient(rgba(146,188,153,1), rgba(226,175,105,1));
opacity: 1;
position: absolute;
top: 828px;
left: 656px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.v2004_59 {
width: 40px;
height: 20px;
background: rgba(179,185,187,1);
opacity: 1;
position: absolute;
top: 108px;
left: 1189px;
border-top-left-radius: 34px;
border-top-right-radius: 34px;
border-bottom-left-radius: 34px;
border-bottom-right-radius: 34px;
overflow: hidden;
}
.v2004_61 {
width: 120px;
height: 68px;
background: rgba(24,144,255,1);
opacity: 1;
position: absolute;
top: 842px;
left: 1176px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.v2004_39 {
width: 160px;
height: 68px;
background: rgba(24,144,255,1);
opacity: 1;
position: absolute;
top: 842px;
left: 919px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.randomPick {
width: 120px;
height: 97px;
color: rgba(0,108,108,1);
background: linear-gradient(rgba(146,188,153,1), rgba(226,175,105,1));
opacity: 1;
position: absolute;
top: 828px;
left: 670px;
border: 0px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
font-family: Microsoft YaHei;
font-weight: Regular;
font-size: 32px;
opacity: 1;
text-align: center;
overflow: hidden;
}
.v2004_47 {
width: 160px;
color: rgba(255,255,255,1);
position: absolute;
top: 852px;
left: 919px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 32px;
opacity: 1;
text-align: center;
}
.v2004_48 {
width: 211px;
height: 48px;
background: rgba(255,255,255,0);
opacity: 1;
position: absolute;
top: 608px;
left: 619px;
border: 1px solid rgba(0,0,0,0.20000000298023224);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.student {
width: 243px;
color: rgba(0,0,0,1);
position: absolute;
top: 600px;
left: 600px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.v2004_49 {
width: 120px;
height: 68px;
background: rgba(24,144,255,1);
opacity: 1;
position: absolute;
top: 722px;
left: 1175px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.v2004_51 {
width: 120px;
color: rgba(255,255,255,1);
position: absolute;
top: 718px;
left: 1175px;
font-family: Microsoft YaHei;
font-weight: Regular;
font-size: 32px;
opacity: 1;
text-align: center;
}
.v2004_55 {
width: 160px;
height: 48px;
background: rgba(179,185,187,1);
opacity: 1;
position: absolute;
top: 722px;
left: 640px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.v2004_65 {
width: 160px;
height: 48px;
background: rgba(179,185,187,1);
opacity: 1;
position: absolute;
top: 722px;
left: 919px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.totalScore {
width: 163px;
color: rgba(0,0,0,1);
position: absolute;
top: 685px;
left: 641px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.v2004_58 {
width: 161px;
color: rgba(255,255,255,1);
position: absolute;
top: 724px;
left: 639px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 36px;
opacity: 1;
text-align: center;
}
.answerScore {
width: 192px;
color: rgba(0,0,0,1);
position: absolute;
top: 670px;
left: 907px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.v2004_67 {
width: 152px;
color: rgba(0,0,0,1);
position: absolute;
top: 728px;
left: 919px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 36px;
opacity: 1;
text-align: center;
}
.v2004_68 {
width: 121px;
color: rgba(255,255,255,1);
position: absolute;
top: 842px;
left: 1175px;
font-family: Microsoft YaHei;
font-weight: Regular;
font-size: 32px;
opacity: 1;
text-align: center;
}
.v2004_45 {
width: 100px;
color: rgba(0,0,0,1);
position: absolute;
top: 100px;
left: 1095px;
font-family: Microsoft YaHei;
font-weight: Regular;
font-size: 20px;
opacity: 1;
text-align: center;
}
.v2052_2 {
width: 20px;
height: 20px;
background: rgba(0,108,108,1);
opacity: 1;
position: absolute;
top: 108px;
left: 1189px;
border-radius: 50%;
}
.success-message {
position: absolute; /* 确保位置是绝对的 */
top: 930px; /* 你期望的垂直位置 */
left: 950px; /* 你期望的水平位置 */
color: green; /* 你可以添加更多样式,比如颜色 */
}
.error-message {
position: absolute; /* 确保位置是绝对的 */
top: 930px; /* 你期望的垂直位置 */
left: 950px; /* 你期望的水平位置 */
color: red; /* 你可以添加更多样式,比如颜色 */
}
</style>

@ -0,0 +1,326 @@
<template>
<div class="student-management-page">
<div>
<div class="bgdiagram_1"></div>
<div class="bgdiagram_5"></div>
<span class="NickName_style">{{nickname}}</span>
<router-link to="/">
<span class="return_style">返回</span>
</router-link>
<span class="show_name">班级名:{{className}}</span>
<label class="upload_student" for="file-upload">上传学生</label>
<input type="file" id="file-upload" @change="uploadFile" accept=".xlsx" />
<!-- 删除所有学生按钮 -->
<button class="drop_student" @click="deleteAllStudents"></button>
</div>
<div v-if="uploadSuccessMessage" class="success-message">{{ uploadSuccessMessage }}</div>
<div v-if="uploadErrorMessage" class="error-message">{{ uploadErrorMessage }}</div>
<table class="student_manage">
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>得分</th>
</tr>
</thead>
<tbody v-if="paginatedStudents.length">
<tr v-for="student in paginatedStudents" :key="student.id">
<td>{{ student.student_id }}</td>
<td>{{ student.name }}</td>
<td>{{ student.scores }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default{
data() {
return {
students: [],
message: '',
uploadSuccessMessage: '', //
uploadErrorMessage: '', //
currentPage: 1, //
pageSize: 20, //
nickname: ''
}
},
mounted() {
this.fetchStudents(); //
this.nickname = localStorage.getItem('nickname'); // localStorage
},
computed: {
className() {
return this.$route.params.className;
},
totalPages() {
return Math.ceil(this.students.length / this.pageSize); //
},
paginatedStudents() {
const start = (this.currentPage - 1) * this.pageSize; //
return this.students.slice(start, start + this.pageSize); //
},
},
methods: {
async fetchStudents() {
try {
const response = await axios.get(`http://127.0.0.1:8000/teacher/pick_student/get_students/?class_name=${this.className}`,{
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.students = response.data;
} catch (error) {
this.message = '获取学生列表失败';
console.error(error);
}
},
async deleteAllStudents() {
try {
const response = await axios.delete(`http://127.0.0.1:8000/teacher/pick_student/delete_students/?class_name=${this.className}`,{
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.message = response.data.message;
this.students = []; //
} catch (error) {
this.message = '删除所有学生失败';
console.error(error);
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
},
async uploadFile(event) {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post(`http://127.0.0.1:8000/teacher/pick_student/upload_students/?class_name=${this.className}`, formData, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
this.uploadSuccessMessage = response.data.message; //
this.uploadErrorMessage = ''; //
this.fetchStudents(); //
setTimeout(() => {
this.uploadSuccessMessage = '';
}, 3000);
event.target.value = ''; //
} catch (error) {
console.error('上传学生信息失败', error);
this.uploadErrorMessage = error.response?.data?.detail || '上传失败,请重试。'; //
this.uploadSuccessMessage = ''; //
setTimeout(() => {
this.uploadErrorMessage = '';
}, 3000);
}
} else {
this.uploadErrorMessage = '请上传文件。';
setTimeout(() => {
this.uploadErrorMessage = '';
}, 3000);
}
}
}
}
</script>
<style scoped>
.bgdiagram_1 {
width: 100%;
height: 300px;
background: rgba(94,162,218,1);
opacity: 1;
position: absolute;
top: 0px;
left: 0px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
overflow: hidden;
}
.bgdiagram_5 {
width: 80%;
height: 60px;
background: rgba(217,217,217,0.4000000059604645);
opacity: 1;
position: absolute;
top: 240px;
left: 120px;
overflow: hidden;
}
.student_manage {
width: 80%;
height: 200px;
background: rgba(255,255,255,1);
opacity: 1;
position: absolute;
top: 324px;
left: 120px;
border: 1px solid rgba(0,0,0,0.10000000149011612);
overflow: hidden;
}
td {
text-align: center;
}
.NickName_style {
width: 198px;
color: rgba(255,255,255,1);
position: absolute;
top: 130px;
left: 120px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.show_name {
width: 160px;
color: rgba(255,255,255,1);
position: absolute;
top: 259px;
left: 150px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.upload_student {
width: 125px;
color: rgba(255,255,255,100);
background-color: rgba(255, 255, 255, 0);
border-width: 1px;
position: absolute;
top: 258px;
left: 400px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.drop_student {
width: 125px;
color: rgba(255,255,255,100);
background-color: rgba(255, 255, 255, 0);
border-width: 0px;
position: absolute;
top: 258px;
left: 600px;
font-family: Microsoft Himalaya;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: center;
}
.button_style {
width: 90px;
height: 40px;
background: rgba(24,144,255,1);
opacity: 1;
color: white;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
}
.return_style {
width: 80px;
color: rgba(255,255,255,1);
position: absolute;
top: 180px;
left: 120px;
font-family: Inter;
font-weight: Regular;
font-size: 24px;
opacity: 1;
text-align: left;
}
.pagination {
top: 850px;
position: relative;
z-index: 10;
text-align: center; /* 居中对齐 */
margin: 20px 0; /* 添加上下边距 */
}
.pagination button {
padding: 10px 20px; /* 添加内边距 */
margin: 0 10px; /* 添加左右边距 */
border: none;
border-radius: 5px; /* 添加圆角 */
background: rgba(24, 144, 255, 1);
color: white;
cursor: pointer;
}
.pagination button:disabled {
background: rgba(200, 200, 200, 0.5); /* 禁用状态颜色 */
cursor: not-allowed; /* 光标样式 */
}
#file-upload {
display: none;
}
.success-message {
position: relative;
z-index: 10;
top: 260px;
left: 800px;
}
.error-message {
position: relative;
z-index: 10;
top: 260px;
left: 800px;
}
</style>

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})

@ -0,0 +1,261 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.25.7":
version "7.25.7"
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz"
integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==
"@babel/helper-validator-identifier@^7.25.7":
version "7.25.7"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz"
integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==
"@babel/parser@^7.25.3":
version "7.25.7"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz"
integrity sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==
dependencies:
"@babel/types" "^7.25.7"
"@babel/types@^7.25.7":
version "7.25.7"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz"
integrity sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==
dependencies:
"@babel/helper-string-parser" "^7.25.7"
"@babel/helper-validator-identifier" "^7.25.7"
to-fast-properties "^2.0.0"
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@rollup/rollup-win32-x64-msvc@4.24.0":
version "4.24.0"
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz"
integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==
"@types/estree@1.0.6":
version "1.0.6"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@vitejs/plugin-vue@^5.1.4":
version "5.1.4"
resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz"
integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
"@vue/compiler-core@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz"
integrity sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.11"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz"
integrity sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==
dependencies:
"@vue/compiler-core" "3.5.11"
"@vue/shared" "3.5.11"
"@vue/compiler-sfc@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz"
integrity sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.11"
"@vue/compiler-dom" "3.5.11"
"@vue/compiler-ssr" "3.5.11"
"@vue/shared" "3.5.11"
estree-walker "^2.0.2"
magic-string "^0.30.11"
postcss "^8.4.47"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz"
integrity sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==
dependencies:
"@vue/compiler-dom" "3.5.11"
"@vue/shared" "3.5.11"
"@vue/reactivity@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.11.tgz"
integrity sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==
dependencies:
"@vue/shared" "3.5.11"
"@vue/runtime-core@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.11.tgz"
integrity sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==
dependencies:
"@vue/reactivity" "3.5.11"
"@vue/shared" "3.5.11"
"@vue/runtime-dom@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.11.tgz"
integrity sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==
dependencies:
"@vue/reactivity" "3.5.11"
"@vue/runtime-core" "3.5.11"
"@vue/shared" "3.5.11"
csstype "^3.1.3"
"@vue/server-renderer@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.11.tgz"
integrity sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==
dependencies:
"@vue/compiler-ssr" "3.5.11"
"@vue/shared" "3.5.11"
"@vue/shared@3.5.11":
version "3.5.11"
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz"
integrity sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz"
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
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"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
magic-string@^0.30.11:
version "0.30.11"
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz"
integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
picocolors@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz"
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
postcss@^8.4.43, postcss@^8.4.47:
version "8.4.47"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.7"
picocolors "^1.1.0"
source-map-js "^1.2.1"
rollup@^4.20.0:
version "4.24.0"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz"
integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.24.0"
"@rollup/rollup-android-arm64" "4.24.0"
"@rollup/rollup-darwin-arm64" "4.24.0"
"@rollup/rollup-darwin-x64" "4.24.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.24.0"
"@rollup/rollup-linux-arm-musleabihf" "4.24.0"
"@rollup/rollup-linux-arm64-gnu" "4.24.0"
"@rollup/rollup-linux-arm64-musl" "4.24.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.0"
"@rollup/rollup-linux-riscv64-gnu" "4.24.0"
"@rollup/rollup-linux-s390x-gnu" "4.24.0"
"@rollup/rollup-linux-x64-gnu" "4.24.0"
"@rollup/rollup-linux-x64-musl" "4.24.0"
"@rollup/rollup-win32-arm64-msvc" "4.24.0"
"@rollup/rollup-win32-ia32-msvc" "4.24.0"
"@rollup/rollup-win32-x64-msvc" "4.24.0"
fsevents "~2.3.2"
source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz"
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
vite@^5.0.0, vite@^5.4.8:
version "5.4.8"
resolved "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"
vue@^3.2.25, vue@^3.5.10, vue@3.5.11:
version "3.5.11"
resolved "https://registry.npmjs.org/vue/-/vue-3.5.11.tgz"
integrity sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==
dependencies:
"@vue/compiler-dom" "3.5.11"
"@vue/compiler-sfc" "3.5.11"
"@vue/runtime-dom" "3.5.11"
"@vue/server-renderer" "3.5.11"
"@vue/shared" "3.5.11"
Loading…
Cancel
Save