diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a3f3b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +backend/app/__pycache__/* +frontend/node_modules/* +backend/app/models/__pycache__/* +backend/app/schemas/__pycache__/* +backend/app/routers/__pycache__/* diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..c7bcbd4 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..9c1be9e --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,92 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.routers import items, auth, users, laboratories, devices, reservations, experiments, attendances, maintenances, notifications, dashboard +from app.database import engine, Base +from app.models.models import User +from app.database import SessionLocal +from app.utils.auth import get_password_hash + +Base.metadata.create_all(bind=engine) + +def init_db(): + db = SessionLocal() + try: + admin = db.query(User).filter(User.username == "admin").first() + if not admin: + admin_user = User( + username="admin", + password=get_password_hash("admin123"), + name="管理员", + email="admin@example.com", + role="admin", + is_active=True + ) + db.add(admin_user) + db.commit() + print("管理员账户已创建: admin / admin123") + + teacher = db.query(User).filter(User.username == "teacher").first() + if not teacher: + teacher_user = User( + username="teacher", + password=get_password_hash("teacher123"), + name="张老师", + email="teacher@example.com", + role="teacher", + is_active=True + ) + db.add(teacher_user) + db.commit() + print("教师账户已创建: teacher / teacher123") + + student = db.query(User).filter(User.username == "student").first() + if not student: + student_user = User( + username="student", + password=get_password_hash("student123"), + name="李学生", + email="student@example.com", + role="student", + student_id="2024001", + major="计算机科学与技术", + grade="2024级", + advisor="张教授", + is_active=True + ) + db.add(student_user) + db.commit() + print("学生账户已创建: student / student123") + finally: + db.close() + +init_db() + +app = FastAPI(title="研究生实验室管理系统", version="1.0.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router, prefix="/api/auth", tags=["认证"]) +app.include_router(users.router, prefix="/api/users", tags=["用户管理"]) +app.include_router(laboratories.router, prefix="/api/laboratories", tags=["实验室管理"]) +app.include_router(devices.router, prefix="/api/devices", tags=["设备管理"]) +app.include_router(reservations.router, prefix="/api/reservations", tags=["预约管理"]) +app.include_router(experiments.router, prefix="/api/experiments", tags=["实验记录"]) +app.include_router(attendances.router, prefix="/api/attendances", tags=["考勤管理"]) +app.include_router(maintenances.router, prefix="/api/maintenances", tags=["维护管理"]) +app.include_router(notifications.router, prefix="/api/notifications", tags=["通知管理"]) +app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"]) +app.include_router(items.router, prefix="/api", tags=["示例项目"]) + +@app.get("/") +def read_root(): + return {"message": "欢迎使用研究生实验室管理系统", "version": "1.0.0"} + +@app.get("/health") +def health_check(): + return {"status": "healthy"} \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/models.py b/backend/app/models/models.py new file mode 100644 index 0000000..52790d3 --- /dev/null +++ b/backend/app/models/models.py @@ -0,0 +1,181 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, Float, Boolean, Date +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, nullable=False, index=True) + password = Column(String(255), nullable=False) + name = Column(String(50), nullable=False) + email = Column(String(100), unique=True, nullable=True) + phone = Column(String(20), nullable=True) + role = Column(String(20), default="student") + student_id = Column(String(20), nullable=True) + major = Column(String(100), nullable=True) + grade = Column(String(20), nullable=True) + advisor = Column(String(50), nullable=True) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + devices = relationship("Device", back_populates="user") + reservations = relationship("Reservation", back_populates="user") + experiments = relationship("Experiment", back_populates="user") + attendances = relationship("Attendance", back_populates="user") + notifications = relationship("Notification", back_populates="user") + +class Laboratory(Base): + __tablename__ = "laboratories" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + code = Column(String(50), unique=True, nullable=False, index=True) + location = Column(String(200), nullable=True) + capacity = Column(Integer, default=0) + manager = Column(String(50), nullable=True) + description = Column(Text, nullable=True) + status = Column(String(20), default="available") + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + devices = relationship("Device", back_populates="laboratory") + reservations = relationship("Reservation", back_populates="laboratory") + experiments = relationship("Experiment", back_populates="laboratory") + +class Device(Base): + __tablename__ = "devices" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + code = Column(String(50), unique=True, nullable=False, index=True) + type = Column(String(50), nullable=True) + model = Column(String(100), nullable=True) + brand = Column(String(100), nullable=True) + serial_number = Column(String(100), nullable=True) + purchase_date = Column(Date, nullable=True) + price = Column(Float, nullable=True) + status = Column(String(20), default="available") + location = Column(String(200), nullable=True) + description = Column(Text, nullable=True) + maintenance_cycle = Column(Integer, default=0) + last_maintenance = Column(Date, nullable=True) + + laboratory_id = Column(Integer, ForeignKey("laboratories.id"), nullable=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + laboratory = relationship("Laboratory", back_populates="devices") + user = relationship("User", back_populates="devices") + reservations = relationship("Reservation", back_populates="device") + maintenances = relationship("Maintenance", back_populates="device") + +class Reservation(Base): + __tablename__ = "reservations" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + purpose = Column(Text, nullable=True) + start_time = Column(DateTime(timezone=True), nullable=False) + end_time = Column(DateTime(timezone=True), nullable=False) + status = Column(String(20), default="pending") + remarks = Column(Text, nullable=True) + + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + laboratory_id = Column(Integer, ForeignKey("laboratories.id"), nullable=True) + device_id = Column(Integer, ForeignKey("devices.id"), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + user = relationship("User", back_populates="reservations") + laboratory = relationship("Laboratory", back_populates="reservations") + device = relationship("Device", back_populates="reservations") + +class Experiment(Base): + __tablename__ = "experiments" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + content = Column(Text, nullable=True) + purpose = Column(Text, nullable=True) + hypothesis = Column(Text, nullable=True) + procedure = Column(Text, nullable=True) + result = Column(Text, nullable=True) + conclusion = Column(Text, nullable=True) + status = Column(String(20), default="draft") + start_date = Column(Date, nullable=True) + end_date = Column(Date, nullable=True) + + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + laboratory_id = Column(Integer, ForeignKey("laboratories.id"), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + user = relationship("User", back_populates="experiments") + laboratory = relationship("Laboratory", back_populates="experiments") + +class Attendance(Base): + __tablename__ = "attendances" + + id = Column(Integer, primary_key=True, index=True) + check_in_time = Column(DateTime(timezone=True), nullable=False) + check_out_time = Column(DateTime(timezone=True), nullable=True) + duration = Column(Float, nullable=True) + date = Column(Date, nullable=False, index=True) + + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + user = relationship("User", back_populates="attendances") + +class Maintenance(Base): + __tablename__ = "maintenances" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + description = Column(Text, nullable=True) + type = Column(String(20), default="routine") + status = Column(String(20), default="scheduled") + scheduled_date = Column(Date, nullable=True) + completed_date = Column(Date, nullable=True) + cost = Column(Float, nullable=True) + remarks = Column(Text, nullable=True) + + device_id = Column(Integer, ForeignKey("devices.id"), nullable=False) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + device = relationship("Device", back_populates="maintenances") + +class Notification(Base): + __tablename__ = "notifications" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + content = Column(Text, nullable=True) + type = Column(String(20), default="info") + is_read = Column(Boolean, default=False) + + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="notifications") + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + description = Column(String(500), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routers/attendances.py b/backend/app/routers/attendances.py new file mode 100644 index 0000000..750c81e --- /dev/null +++ b/backend/app/routers/attendances.py @@ -0,0 +1,145 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload +from typing import List, Optional +from datetime import datetime, date +from app.database import get_db +from app.models.models import Attendance, User +from app.schemas.schemas import Attendance as AttendanceSchema, AttendanceCreate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[AttendanceSchema]) +def read_attendances( + skip: int = 0, + limit: int = 100, + user_id: Optional[int] = None, + date_from: Optional[date] = None, + date_to: Optional[date] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Attendance).options(joinedload(Attendance.user)) + + if current_user.role == "student": + query = query.filter(Attendance.user_id == current_user.id) + elif user_id: + query = query.filter(Attendance.user_id == user_id) + + if date_from: + query = query.filter(Attendance.date >= date_from) + if date_to: + query = query.filter(Attendance.date <= date_to) + + attendances = query.offset(skip).limit(limit).all() + return attendances + +@router.get("/my", response_model=List[AttendanceSchema]) +def read_my_attendances( + skip: int = 0, + limit: int = 100, + date_from: Optional[date] = None, + date_to: Optional[date] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Attendance).options(joinedload(Attendance.user)).filter( + Attendance.user_id == current_user.id + ) + + if date_from: + query = query.filter(Attendance.date >= date_from) + if date_to: + query = query.filter(Attendance.date <= date_to) + + attendances = query.offset(skip).limit(limit).all() + return attendances + +@router.get("/today", response_model=Optional[AttendanceSchema]) +def read_today_attendance( + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + today = date.today() + attendance = db.query(Attendance).options(joinedload(Attendance.user)).filter( + Attendance.user_id == current_user.id, + Attendance.date == today + ).first() + return attendance + +@router.post("/check-in", response_model=AttendanceSchema) +def check_in( + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + today = date.today() + existing_attendance = db.query(Attendance).filter( + Attendance.user_id == current_user.id, + Attendance.date == today + ).first() + + if existing_attendance and existing_attendance.check_in_time: + raise HTTPException(status_code=400, detail="今日已签到") + + now = datetime.now() + if existing_attendance: + existing_attendance.check_in_time = now + db.commit() + db.refresh(existing_attendance) + return existing_attendance + else: + new_attendance = Attendance( + user_id=current_user.id, + check_in_time=now, + date=today + ) + db.add(new_attendance) + db.commit() + db.refresh(new_attendance) + return new_attendance + +@router.post("/check-out", response_model=AttendanceSchema) +def check_out( + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + today = date.today() + attendance = db.query(Attendance).filter( + Attendance.user_id == current_user.id, + Attendance.date == today + ).first() + + if not attendance: + raise HTTPException(status_code=400, detail="今日未签到") + + if attendance.check_out_time: + raise HTTPException(status_code=400, detail="今日已签退") + + now = datetime.now() + attendance.check_out_time = now + + if attendance.check_in_time: + duration = (now - attendance.check_in_time).total_seconds() / 3600 + attendance.duration = round(duration, 2) + + db.commit() + db.refresh(attendance) + return attendance + +@router.get("/{attendance_id}", response_model=AttendanceSchema) +def read_attendance( + attendance_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + attendance = db.query(Attendance).options(joinedload(Attendance.user)).filter( + Attendance.id == attendance_id + ).first() + + if attendance is None: + raise HTTPException(status_code=404, detail="考勤记录不存在") + + if current_user.role == "student" and attendance.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return attendance \ No newline at end of file diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..0d0a4ba --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,98 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from typing import Optional +from app.database import get_db +from app.models.models import User +from app.schemas.schemas import User as UserSchema, UserCreate, Token +from app.utils.auth import verify_password, get_password_hash, create_access_token, decode_token + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + +async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> Optional[User]: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无法验证凭据", + headers={"WWW-Authenticate": "Bearer"}, + ) + payload = decode_token(token) + if payload is None: + raise credentials_exception + user_id: int = payload.get("user_id") + if user_id is None: + raise credentials_exception + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise credentials_exception + return user + +async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + if not current_user.is_active: + raise HTTPException(status_code=400, detail="用户已被禁用") + return current_user + +@router.post("/register", response_model=UserSchema) +def register(user_data: UserCreate, db: Session = Depends(get_db)): + existing_user = db.query(User).filter(User.username == user_data.username).first() + if existing_user: + raise HTTPException(status_code=400, detail="用户名已存在") + if user_data.email: + existing_email = db.query(User).filter(User.email == user_data.email).first() + if existing_email: + raise HTTPException(status_code=400, detail="邮箱已被注册") + + hashed_password = get_password_hash(user_data.password) + new_user = User( + username=user_data.username, + password=hashed_password, + name=user_data.name, + email=user_data.email, + phone=user_data.phone, + role=user_data.role, + student_id=user_data.student_id, + major=user_data.major, + grade=user_data.grade, + advisor=user_data.advisor + ) + db.add(new_user) + db.commit() + db.refresh(new_user) + return new_user + +@router.post("/login", response_model=Token) +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == form_data.username).first() + if not user or not verify_password(form_data.password, user.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户名或密码错误", + headers={"WWW-Authenticate": "Bearer"}, + ) + if not user.is_active: + raise HTTPException(status_code=400, detail="用户已被禁用") + + access_token = create_access_token(data={"user_id": user.id, "username": user.username, "role": user.role}) + return Token(access_token=access_token, user=user) + +@router.get("/me", response_model=UserSchema) +def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + +@router.put("/me", response_model=UserSchema) +def update_user_me( + name: str = None, + email: str = None, + phone: str = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if name: + current_user.name = name + if email: + current_user.email = email + if phone: + current_user.phone = phone + db.commit() + db.refresh(current_user) + return current_user \ No newline at end of file diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py new file mode 100644 index 0000000..3dbe428 --- /dev/null +++ b/backend/app/routers/dashboard.py @@ -0,0 +1,137 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from sqlalchemy import func +from datetime import date, datetime, timedelta +from app.database import get_db +from app.models.models import User, Laboratory, Device, Experiment, Reservation, Attendance +from app.schemas.schemas import DashboardStats +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/stats", response_model=DashboardStats) +def get_dashboard_stats( + current_user = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + today = date.today() + + total_users = db.query(User).count() + total_laboratories = db.query(Laboratory).count() + total_devices = db.query(Device).count() + total_experiments = db.query(Experiment).count() + + today_attendances = db.query(Attendance).filter( + Attendance.date == today + ).count() + + pending_reservations = db.query(Reservation).filter( + Reservation.status == "pending" + ).count() + + active_devices = db.query(Device).filter( + Device.status == "in_use" + ).count() + + maintenance_devices = db.query(Device).filter( + Device.status == "maintenance" + ).count() + + return DashboardStats( + total_users=total_users, + total_laboratories=total_laboratories, + total_devices=total_devices, + total_experiments=total_experiments, + today_attendances=today_attendances, + pending_reservations=pending_reservations, + active_devices=active_devices, + maintenance_devices=maintenance_devices + ) + +@router.get("/recent-activities") +def get_recent_activities( + limit: int = 10, + current_user = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + activities = [] + + recent_experiments = db.query(Experiment).order_by( + Experiment.created_at.desc() + ).limit(limit).all() + + for exp in recent_experiments: + activities.append({ + "id": exp.id, + "type": "experiment", + "title": f"实验: {exp.title}", + "status": exp.status, + "created_at": exp.created_at + }) + + recent_reservations = db.query(Reservation).order_by( + Reservation.created_at.desc() + ).limit(limit).all() + + for res in recent_reservations: + activities.append({ + "id": res.id, + "type": "reservation", + "title": f"预约: {res.title}", + "status": res.status, + "created_at": res.created_at + }) + + activities.sort(key=lambda x: x["created_at"], reverse=True) + return activities[:limit] + +@router.get("/monthly-stats") +def get_monthly_stats( + current_user = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + today = date.today() + first_day = today.replace(day=1) + + monthly_attendances = db.query( + func.date(Attendance.date).label('date'), + func.count(Attendance.id).label('count') + ).filter( + Attendance.date >= first_day + ).group_by( + func.date(Attendance.date) + ).all() + + monthly_experiments = db.query( + func.date(Experiment.created_at).label('date'), + func.count(Experiment.id).label('count') + ).filter( + Experiment.created_at >= first_day + ).group_by( + func.date(Experiment.created_at) + ).all() + + return { + "attendances": [{"date": str(row.date), "count": row.count} for row in monthly_attendances], + "experiments": [{"date": str(row.date), "count": row.count} for row in monthly_experiments] + } + +@router.get("/device-distribution") +def get_device_distribution( + current_user = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + status_distribution = db.query( + Device.status, + func.count(Device.id).label('count') + ).group_by(Device.status).all() + + type_distribution = db.query( + Device.type, + func.count(Device.id).label('count') + ).filter(Device.type != None).group_by(Device.type).all() + + return { + "status": [{"status": row.status, "count": row.count} for row in status_distribution], + "type": [{"type": row.type, "count": row.count} for row in type_distribution] + } \ No newline at end of file diff --git a/backend/app/routers/devices.py b/backend/app/routers/devices.py new file mode 100644 index 0000000..7d55c49 --- /dev/null +++ b/backend/app/routers/devices.py @@ -0,0 +1,98 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload +from typing import List, Optional +from app.database import get_db +from app.models.models import Device, User, Laboratory +from app.schemas.schemas import Device as DeviceSchema, DeviceCreate, DeviceUpdate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[DeviceSchema]) +def read_devices( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + type: Optional[str] = None, + laboratory_id: Optional[int] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Device).options(joinedload(Device.laboratory), joinedload(Device.user)) + if status: + query = query.filter(Device.status == status) + if type: + query = query.filter(Device.type == type) + if laboratory_id: + query = query.filter(Device.laboratory_id == laboratory_id) + devices = query.offset(skip).limit(limit).all() + return devices + +@router.get("/{device_id}", response_model=DeviceSchema) +def read_device( + device_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + device = db.query(Device).options(joinedload(Device.laboratory), joinedload(Device.user)).filter(Device.id == device_id).first() + if device is None: + raise HTTPException(status_code=404, detail="设备不存在") + return device + +@router.post("/", response_model=DeviceSchema) +def create_device( + device_data: DeviceCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + existing_device = db.query(Device).filter(Device.code == device_data.code).first() + if existing_device: + raise HTTPException(status_code=400, detail="设备编号已存在") + + new_device = Device(**device_data.dict()) + db.add(new_device) + db.commit() + db.refresh(new_device) + return new_device + +@router.put("/{device_id}", response_model=DeviceSchema) +def update_device( + device_id: int, + device_data: DeviceUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + device = db.query(Device).filter(Device.id == device_id).first() + if device is None: + raise HTTPException(status_code=404, detail="设备不存在") + + update_data = device_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(device, key, value) + + db.commit() + db.refresh(device) + return device + +@router.delete("/{device_id}") +def delete_device( + device_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="权限不足") + + device = db.query(Device).filter(Device.id == device_id).first() + if device is None: + raise HTTPException(status_code=404, detail="设备不存在") + + db.delete(device) + db.commit() + return {"message": "设备已删除"} \ No newline at end of file diff --git a/backend/app/routers/experiments.py b/backend/app/routers/experiments.py new file mode 100644 index 0000000..cda09e4 --- /dev/null +++ b/backend/app/routers/experiments.py @@ -0,0 +1,128 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload +from typing import List, Optional +from app.database import get_db +from app.models.models import Experiment, User, Laboratory +from app.schemas.schemas import Experiment as ExperimentSchema, ExperimentCreate, ExperimentUpdate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[ExperimentSchema]) +def read_experiments( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + user_id: Optional[int] = None, + laboratory_id: Optional[int] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Experiment).options( + joinedload(Experiment.user), + joinedload(Experiment.laboratory) + ) + + if status: + query = query.filter(Experiment.status == status) + if user_id: + query = query.filter(Experiment.user_id == user_id) + if laboratory_id: + query = query.filter(Experiment.laboratory_id == laboratory_id) + + if current_user.role == "student": + query = query.filter(Experiment.user_id == current_user.id) + + experiments = query.offset(skip).limit(limit).all() + return experiments + +@router.get("/my", response_model=List[ExperimentSchema]) +def read_my_experiments( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Experiment).options( + joinedload(Experiment.user), + joinedload(Experiment.laboratory) + ).filter(Experiment.user_id == current_user.id) + + if status: + query = query.filter(Experiment.status == status) + + experiments = query.offset(skip).limit(limit).all() + return experiments + +@router.get("/{experiment_id}", response_model=ExperimentSchema) +def read_experiment( + experiment_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + experiment = db.query(Experiment).options( + joinedload(Experiment.user), + joinedload(Experiment.laboratory) + ).filter(Experiment.id == experiment_id).first() + + if experiment is None: + raise HTTPException(status_code=404, detail="实验记录不存在") + + if current_user.role == "student" and experiment.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return experiment + +@router.post("/", response_model=ExperimentSchema) +def create_experiment( + experiment_data: ExperimentCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + new_experiment = Experiment(**experiment_data.dict(), user_id=current_user.id) + db.add(new_experiment) + db.commit() + db.refresh(new_experiment) + return new_experiment + +@router.put("/{experiment_id}", response_model=ExperimentSchema) +def update_experiment( + experiment_id: int, + experiment_data: ExperimentUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + experiment = db.query(Experiment).filter(Experiment.id == experiment_id).first() + + if experiment is None: + raise HTTPException(status_code=404, detail="实验记录不存在") + + if current_user.role == "student" and experiment.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + update_data = experiment_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(experiment, key, value) + + db.commit() + db.refresh(experiment) + return experiment + +@router.delete("/{experiment_id}") +def delete_experiment( + experiment_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + experiment = db.query(Experiment).filter(Experiment.id == experiment_id).first() + + if experiment is None: + raise HTTPException(status_code=404, detail="实验记录不存在") + + if current_user.role not in ["admin", "teacher"] and experiment.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + db.delete(experiment) + db.commit() + return {"message": "实验记录已删除"} \ No newline at end of file diff --git a/backend/app/routers/items.py b/backend/app/routers/items.py new file mode 100644 index 0000000..4ef960b --- /dev/null +++ b/backend/app/routers/items.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from app.database import get_db +from app.models.models import Item +from app.schemas.schemas import Item as ItemSchema, ItemCreate, ItemUpdate + +router = APIRouter() + +@router.get("/items/", response_model=List[ItemSchema]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = db.query(Item).offset(skip).limit(limit).all() + return items + +@router.get("/items/{item_id}", response_model=ItemSchema) +def read_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(Item).filter(Item.id == item_id).first() + if db_item is None: + raise HTTPException(status_code=404, detail="Item not found") + return db_item + +@router.post("/items/", response_model=ItemSchema) +def create_item(item: ItemCreate, db: Session = Depends(get_db)): + db_item = Item(name=item.name, description=item.description) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + +@router.put("/items/{item_id}", response_model=ItemSchema) +def update_item(item_id: int, item: ItemUpdate, db: Session = Depends(get_db)): + db_item = db.query(Item).filter(Item.id == item_id).first() + if db_item is None: + raise HTTPException(status_code=404, detail="Item not found") + for key, value in item.dict(exclude_unset=True).items(): + setattr(db_item, key, value) + db.commit() + db.refresh(db_item) + return db_item + +@router.delete("/items/{item_id}") +def delete_item(item_id: int, db: Session = Depends(get_db)): + db_item = db.query(Item).filter(Item.id == item_id).first() + if db_item is None: + raise HTTPException(status_code=404, detail="Item not found") + db.delete(db_item) + db.commit() + return {"message": "Item deleted successfully"} \ No newline at end of file diff --git a/backend/app/routers/laboratories.py b/backend/app/routers/laboratories.py new file mode 100644 index 0000000..272f454 --- /dev/null +++ b/backend/app/routers/laboratories.py @@ -0,0 +1,92 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from app.database import get_db +from app.models.models import Laboratory, User +from app.schemas.schemas import Laboratory as LaboratorySchema, LaboratoryCreate, LaboratoryUpdate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[LaboratorySchema]) +def read_laboratories( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Laboratory) + if status: + query = query.filter(Laboratory.status == status) + laboratories = query.offset(skip).limit(limit).all() + return laboratories + +@router.get("/{lab_id}", response_model=LaboratorySchema) +def read_laboratory( + lab_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + laboratory = db.query(Laboratory).filter(Laboratory.id == lab_id).first() + if laboratory is None: + raise HTTPException(status_code=404, detail="实验室不存在") + return laboratory + +@router.post("/", response_model=LaboratorySchema) +def create_laboratory( + lab_data: LaboratoryCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + existing_lab = db.query(Laboratory).filter(Laboratory.code == lab_data.code).first() + if existing_lab: + raise HTTPException(status_code=400, detail="实验室编号已存在") + + new_lab = Laboratory(**lab_data.dict()) + db.add(new_lab) + db.commit() + db.refresh(new_lab) + return new_lab + +@router.put("/{lab_id}", response_model=LaboratorySchema) +def update_laboratory( + lab_id: int, + lab_data: LaboratoryUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + laboratory = db.query(Laboratory).filter(Laboratory.id == lab_id).first() + if laboratory is None: + raise HTTPException(status_code=404, detail="实验室不存在") + + update_data = lab_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(laboratory, key, value) + + db.commit() + db.refresh(laboratory) + return laboratory + +@router.delete("/{lab_id}") +def delete_laboratory( + lab_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="权限不足") + + laboratory = db.query(Laboratory).filter(Laboratory.id == lab_id).first() + if laboratory is None: + raise HTTPException(status_code=404, detail="实验室不存在") + + db.delete(laboratory) + db.commit() + return {"message": "实验室已删除"} \ No newline at end of file diff --git a/backend/app/routers/maintenances.py b/backend/app/routers/maintenances.py new file mode 100644 index 0000000..160e832 --- /dev/null +++ b/backend/app/routers/maintenances.py @@ -0,0 +1,101 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload +from typing import List, Optional +from app.database import get_db +from app.models.models import Maintenance, Device, User +from app.schemas.schemas import Maintenance as MaintenanceSchema, MaintenanceCreate, MaintenanceUpdate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[MaintenanceSchema]) +def read_maintenances( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + type: Optional[str] = None, + device_id: Optional[int] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Maintenance).options(joinedload(Maintenance.device)) + + if status: + query = query.filter(Maintenance.status == status) + if type: + query = query.filter(Maintenance.type == type) + if device_id: + query = query.filter(Maintenance.device_id == device_id) + + maintenances = query.offset(skip).limit(limit).all() + return maintenances + +@router.get("/{maintenance_id}", response_model=MaintenanceSchema) +def read_maintenance( + maintenance_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + maintenance = db.query(Maintenance).options(joinedload(Maintenance.device)).filter( + Maintenance.id == maintenance_id + ).first() + + if maintenance is None: + raise HTTPException(status_code=404, detail="维护记录不存在") + return maintenance + +@router.post("/", response_model=MaintenanceSchema) +def create_maintenance( + maintenance_data: MaintenanceCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + new_maintenance = Maintenance(**maintenance_data.dict()) + db.add(new_maintenance) + db.commit() + db.refresh(new_maintenance) + return new_maintenance + +@router.put("/{maintenance_id}", response_model=MaintenanceSchema) +def update_maintenance( + maintenance_id: int, + maintenance_data: MaintenanceUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + maintenance = db.query(Maintenance).filter(Maintenance.id == maintenance_id).first() + + if maintenance is None: + raise HTTPException(status_code=404, detail="维护记录不存在") + + update_data = maintenance_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(maintenance, key, value) + + db.commit() + db.refresh(maintenance) + return maintenance + +@router.delete("/{maintenance_id}") +def delete_maintenance( + maintenance_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="权限不足") + + maintenance = db.query(Maintenance).filter(Maintenance.id == maintenance_id).first() + + if maintenance is None: + raise HTTPException(status_code=404, detail="维护记录不存在") + + db.delete(maintenance) + db.commit() + return {"message": "维护记录已删除"} \ No newline at end of file diff --git a/backend/app/routers/notifications.py b/backend/app/routers/notifications.py new file mode 100644 index 0000000..88c62cf --- /dev/null +++ b/backend/app/routers/notifications.py @@ -0,0 +1,128 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from app.database import get_db +from app.models.models import Notification, User +from app.schemas.schemas import Notification as NotificationSchema, NotificationCreate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[NotificationSchema]) +def read_notifications( + skip: int = 0, + limit: int = 100, + is_read: Optional[bool] = None, + type: Optional[str] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Notification).filter( + (Notification.user_id == current_user.id) | (Notification.user_id == None) + ) + + if is_read is not None: + query = query.filter(Notification.is_read == is_read) + if type: + query = query.filter(Notification.type == type) + + notifications = query.offset(skip).limit(limit).all() + return notifications + +@router.get("/unread-count") +def get_unread_count( + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + count = db.query(Notification).filter( + (Notification.user_id == current_user.id) | (Notification.user_id == None), + Notification.is_read == False + ).count() + return {"count": count} + +@router.get("/{notification_id}", response_model=NotificationSchema) +def read_notification( + notification_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + notification = db.query(Notification).filter( + Notification.id == notification_id + ).first() + + if notification is None: + raise HTTPException(status_code=404, detail="通知不存在") + + if notification.user_id and notification.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return notification + +@router.post("/", response_model=NotificationSchema) +def create_notification( + notification_data: NotificationCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + new_notification = Notification(**notification_data.dict()) + db.add(new_notification) + db.commit() + db.refresh(new_notification) + return new_notification + +@router.put("/{notification_id}/read", response_model=NotificationSchema) +def mark_as_read( + notification_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + notification = db.query(Notification).filter( + Notification.id == notification_id + ).first() + + if notification is None: + raise HTTPException(status_code=404, detail="通知不存在") + + if notification.user_id and notification.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + notification.is_read = True + db.commit() + db.refresh(notification) + return notification + +@router.put("/read-all") +def mark_all_as_read( + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + db.query(Notification).filter( + (Notification.user_id == current_user.id) | (Notification.user_id == None), + Notification.is_read == False + ).update({"is_read": True}) + db.commit() + return {"message": "所有通知已标记为已读"} + +@router.delete("/{notification_id}") +def delete_notification( + notification_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + notification = db.query(Notification).filter( + Notification.id == notification_id + ).first() + + if notification is None: + raise HTTPException(status_code=404, detail="通知不存在") + + if current_user.role not in ["admin", "teacher"]: + if notification.user_id and notification.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + db.delete(notification) + db.commit() + return {"message": "通知已删除"} \ No newline at end of file diff --git a/backend/app/routers/reservations.py b/backend/app/routers/reservations.py new file mode 100644 index 0000000..6f4f73b --- /dev/null +++ b/backend/app/routers/reservations.py @@ -0,0 +1,136 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload +from typing import List, Optional +from datetime import datetime +from app.database import get_db +from app.models.models import Reservation, User, Laboratory, Device +from app.schemas.schemas import Reservation as ReservationSchema, ReservationCreate, ReservationUpdate +from app.routers.auth import get_current_active_user + +router = APIRouter() + +@router.get("/", response_model=List[ReservationSchema]) +def read_reservations( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + user_id: Optional[int] = None, + laboratory_id: Optional[int] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Reservation).options( + joinedload(Reservation.user), + joinedload(Reservation.laboratory), + joinedload(Reservation.device) + ) + + if status: + query = query.filter(Reservation.status == status) + if user_id: + query = query.filter(Reservation.user_id == user_id) + if laboratory_id: + query = query.filter(Reservation.laboratory_id == laboratory_id) + + if current_user.role == "student": + query = query.filter(Reservation.user_id == current_user.id) + + reservations = query.offset(skip).limit(limit).all() + return reservations + +@router.get("/my", response_model=List[ReservationSchema]) +def read_my_reservations( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Reservation).options( + joinedload(Reservation.user), + joinedload(Reservation.laboratory), + joinedload(Reservation.device) + ).filter(Reservation.user_id == current_user.id) + + if status: + query = query.filter(Reservation.status == status) + + reservations = query.offset(skip).limit(limit).all() + return reservations + +@router.get("/{reservation_id}", response_model=ReservationSchema) +def read_reservation( + reservation_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + reservation = db.query(Reservation).options( + joinedload(Reservation.user), + joinedload(Reservation.laboratory), + joinedload(Reservation.device) + ).filter(Reservation.id == reservation_id).first() + + if reservation is None: + raise HTTPException(status_code=404, detail="预约不存在") + + if current_user.role == "student" and reservation.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return reservation + +@router.post("/", response_model=ReservationSchema) +def create_reservation( + reservation_data: ReservationCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + new_reservation = Reservation(**reservation_data.dict(), user_id=current_user.id) + db.add(new_reservation) + db.commit() + db.refresh(new_reservation) + return new_reservation + +@router.put("/{reservation_id}", response_model=ReservationSchema) +def update_reservation( + reservation_id: int, + reservation_data: ReservationUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + reservation = db.query(Reservation).filter(Reservation.id == reservation_id).first() + + if reservation is None: + raise HTTPException(status_code=404, detail="预约不存在") + + if current_user.role == "student" and reservation.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + update_data = reservation_data.dict(exclude_unset=True) + + if "status" in update_data and current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="无权修改预约状态") + + for key, value in update_data.items(): + setattr(reservation, key, value) + + db.commit() + db.refresh(reservation) + return reservation + +@router.delete("/{reservation_id}") +def delete_reservation( + reservation_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + reservation = db.query(Reservation).filter(Reservation.id == reservation_id).first() + + if reservation is None: + raise HTTPException(status_code=404, detail="预约不存在") + + if current_user.role not in ["admin", "teacher"] and reservation.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + db.delete(reservation) + db.commit() + return {"message": "预约已取消"} \ No newline at end of file diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py new file mode 100644 index 0000000..46d3fe3 --- /dev/null +++ b/backend/app/routers/users.py @@ -0,0 +1,108 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from app.database import get_db +from app.models.models import User +from app.schemas.schemas import User as UserSchema, UserCreate, UserUpdate +from app.routers.auth import get_current_active_user +from app.utils.auth import get_password_hash + +router = APIRouter() + +@router.get("/", response_model=List[UserSchema]) +def read_users( + skip: int = 0, + limit: int = 100, + role: Optional[str] = None, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(User) + if role: + query = query.filter(User.role == role) + if is_active is not None: + query = query.filter(User.is_active == is_active) + users = query.offset(skip).limit(limit).all() + return users + +@router.get("/{user_id}", response_model=UserSchema) +def read_user( + user_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException(status_code=404, detail="用户不存在") + return user + +@router.post("/", response_model=UserSchema) +def create_user( + user_data: UserCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"]: + raise HTTPException(status_code=403, detail="权限不足") + + existing_user = db.query(User).filter(User.username == user_data.username).first() + if existing_user: + raise HTTPException(status_code=400, detail="用户名已存在") + + hashed_password = get_password_hash(user_data.password) + new_user = User( + username=user_data.username, + password=hashed_password, + name=user_data.name, + email=user_data.email, + phone=user_data.phone, + role=user_data.role, + student_id=user_data.student_id, + major=user_data.major, + grade=user_data.grade, + advisor=user_data.advisor + ) + db.add(new_user) + db.commit() + db.refresh(new_user) + return new_user + +@router.put("/{user_id}", response_model=UserSchema) +def update_user( + user_id: int, + user_data: UserUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role not in ["admin", "teacher"] and current_user.id != user_id: + raise HTTPException(status_code=403, detail="权限不足") + + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException(status_code=404, detail="用户不存在") + + update_data = user_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(user, key, value) + + db.commit() + db.refresh(user) + return user + +@router.delete("/{user_id}") +def delete_user( + user_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if current_user.role != "admin": + raise HTTPException(status_code=403, detail="权限不足") + + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException(status_code=404, detail="用户不存在") + + db.delete(user) + db.commit() + return {"message": "用户已删除"} \ No newline at end of file diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py new file mode 100644 index 0000000..c538bfe --- /dev/null +++ b/backend/app/schemas/schemas.py @@ -0,0 +1,330 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional, List +from datetime import datetime, date +from enum import Enum + +class UserRole(str, Enum): + ADMIN = "admin" + TEACHER = "teacher" + STUDENT = "student" + ASSISTANT = "assistant" + +class StatusEnum(str, Enum): + AVAILABLE = "available" + IN_USE = "in_use" + MAINTENANCE = "maintenance" + UNAVAILABLE = "unavailable" + +class ReservationStatus(str, Enum): + PENDING = "pending" + APPROVED = "approved" + REJECTED = "rejected" + CANCELLED = "cancelled" + COMPLETED = "completed" + +class ExperimentStatus(str, Enum): + DRAFT = "draft" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + PUBLISHED = "published" + +class MaintenanceType(str, Enum): + ROUTINE = "routine" + REPAIR = "repair" + UPGRADE = "upgrade" + INSPECTION = "inspection" + +class NotificationType(str, Enum): + INFO = "info" + WARNING = "warning" + ERROR = "error" + SUCCESS = "success" + +class UserBase(BaseModel): + username: str + name: str + email: Optional[EmailStr] = None + phone: Optional[str] = None + role: str = "student" + student_id: Optional[str] = None + major: Optional[str] = None + grade: Optional[str] = None + advisor: Optional[str] = None + +class UserCreate(UserBase): + password: str + +class UserUpdate(BaseModel): + name: Optional[str] = None + email: Optional[EmailStr] = None + phone: Optional[str] = None + role: Optional[str] = None + student_id: Optional[str] = None + major: Optional[str] = None + grade: Optional[str] = None + advisor: Optional[str] = None + is_active: Optional[bool] = None + +class UserLogin(BaseModel): + username: str + password: str + +class User(UserBase): + id: int + is_active: bool + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class Token(BaseModel): + access_token: str + token_type: str = "bearer" + user: User + +class LaboratoryBase(BaseModel): + name: str + code: str + location: Optional[str] = None + capacity: int = 0 + manager: Optional[str] = None + description: Optional[str] = None + status: str = "available" + +class LaboratoryCreate(LaboratoryBase): + pass + +class LaboratoryUpdate(BaseModel): + name: Optional[str] = None + code: Optional[str] = None + location: Optional[str] = None + capacity: Optional[int] = None + manager: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + +class Laboratory(LaboratoryBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class DeviceBase(BaseModel): + name: str + code: str + type: Optional[str] = None + model: Optional[str] = None + brand: Optional[str] = None + serial_number: Optional[str] = None + purchase_date: Optional[date] = None + price: Optional[float] = None + status: str = "available" + location: Optional[str] = None + description: Optional[str] = None + maintenance_cycle: int = 0 + last_maintenance: Optional[date] = None + laboratory_id: Optional[int] = None + user_id: Optional[int] = None + +class DeviceCreate(DeviceBase): + pass + +class DeviceUpdate(BaseModel): + name: Optional[str] = None + code: Optional[str] = None + type: Optional[str] = None + model: Optional[str] = None + brand: Optional[str] = None + serial_number: Optional[str] = None + purchase_date: Optional[date] = None + price: Optional[float] = None + status: Optional[str] = None + location: Optional[str] = None + description: Optional[str] = None + maintenance_cycle: Optional[int] = None + last_maintenance: Optional[date] = None + laboratory_id: Optional[int] = None + user_id: Optional[int] = None + +class Device(DeviceBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + laboratory: Optional[Laboratory] = None + user: Optional[User] = None + + class Config: + from_attributes = True + +class ReservationBase(BaseModel): + title: str + purpose: Optional[str] = None + start_time: datetime + end_time: datetime + remarks: Optional[str] = None + laboratory_id: Optional[int] = None + device_id: Optional[int] = None + +class ReservationCreate(ReservationBase): + pass + +class ReservationUpdate(BaseModel): + title: Optional[str] = None + purpose: Optional[str] = None + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + status: Optional[str] = None + remarks: Optional[str] = None + +class Reservation(ReservationBase): + id: int + user_id: int + status: str + created_at: datetime + updated_at: Optional[datetime] = None + user: Optional[User] = None + laboratory: Optional[Laboratory] = None + device: Optional[Device] = None + + class Config: + from_attributes = True + +class ExperimentBase(BaseModel): + title: str + content: Optional[str] = None + purpose: Optional[str] = None + hypothesis: Optional[str] = None + procedure: Optional[str] = None + result: Optional[str] = None + conclusion: Optional[str] = None + status: str = "draft" + start_date: Optional[date] = None + end_date: Optional[date] = None + laboratory_id: Optional[int] = None + +class ExperimentCreate(ExperimentBase): + pass + +class ExperimentUpdate(BaseModel): + title: Optional[str] = None + content: Optional[str] = None + purpose: Optional[str] = None + hypothesis: Optional[str] = None + procedure: Optional[str] = None + result: Optional[str] = None + conclusion: Optional[str] = None + status: Optional[str] = None + start_date: Optional[date] = None + end_date: Optional[date] = None + laboratory_id: Optional[int] = None + +class Experiment(ExperimentBase): + id: int + user_id: int + created_at: datetime + updated_at: Optional[datetime] = None + user: Optional[User] = None + laboratory: Optional[Laboratory] = None + + class Config: + from_attributes = True + +class AttendanceBase(BaseModel): + check_in_time: datetime + check_out_time: Optional[datetime] = None + +class AttendanceCreate(AttendanceBase): + pass + +class Attendance(AttendanceBase): + id: int + user_id: int + duration: Optional[float] = None + date: date + created_at: datetime + updated_at: Optional[datetime] = None + user: Optional[User] = None + + class Config: + from_attributes = True + +class MaintenanceBase(BaseModel): + title: str + description: Optional[str] = None + type: str = "routine" + status: str = "scheduled" + scheduled_date: Optional[date] = None + completed_date: Optional[date] = None + cost: Optional[float] = None + remarks: Optional[str] = None + +class MaintenanceCreate(MaintenanceBase): + device_id: int + +class MaintenanceUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + type: Optional[str] = None + status: Optional[str] = None + scheduled_date: Optional[date] = None + completed_date: Optional[date] = None + cost: Optional[float] = None + remarks: Optional[str] = None + +class Maintenance(MaintenanceBase): + id: int + device_id: int + created_at: datetime + updated_at: Optional[datetime] = None + device: Optional[Device] = None + + class Config: + from_attributes = True + +class NotificationBase(BaseModel): + title: str + content: Optional[str] = None + type: str = "info" + +class NotificationCreate(NotificationBase): + user_id: Optional[int] = None + +class Notification(NotificationBase): + id: int + is_read: bool + user_id: Optional[int] = None + created_at: datetime + + class Config: + from_attributes = True + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + pass + +class Item(ItemBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class DashboardStats(BaseModel): + total_users: int + total_laboratories: int + total_devices: int + total_experiments: int + today_attendances: int + pending_reservations: int + active_devices: int + maintenance_devices: int \ No newline at end of file diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/__pycache__/__init__.cpython-311.pyc b/backend/app/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1312dbb Binary files /dev/null and b/backend/app/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/utils/__pycache__/auth.cpython-311.pyc b/backend/app/utils/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..ddcc27f Binary files /dev/null and b/backend/app/utils/__pycache__/auth.cpython-311.pyc differ diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py new file mode 100644 index 0000000..b1cdbc8 --- /dev/null +++ b/backend/app/utils/auth.py @@ -0,0 +1,47 @@ +from datetime import datetime, timedelta +from typing import Optional +import hashlib +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + SECRET_KEY: str = "your-secret-key-change-in-production-2024" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 + +settings = Settings() + +pwd_context = CryptContext( + schemes=["bcrypt"], + deprecated="auto", + bcrypt__rounds=12 +) + +def _hash_password_sha256(password: str) -> str: + return hashlib.sha256(password.encode('utf-8')).hexdigest() + +def verify_password(plain_password: str, hashed_password: str) -> bool: + sha256_password = _hash_password_sha256(plain_password) + return pwd_context.verify(sha256_password, hashed_password) + +def get_password_hash(password: str) -> str: + sha256_password = _hash_password_sha256(password) + return pwd_context.hash(sha256_password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + +def decode_token(token: str) -> Optional[dict]: + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + return payload + except JWTError: + return None \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..fc6821d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi==0.109.0 +uvicorn==0.27.0 +sqlalchemy==2.0.25 +pydantic==2.5.3 +pydantic-settings==2.1.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..bdc0171 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 研究生实验室管理系统 + + +
+ + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5d699bc --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1978 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.5", + "element-plus": "^2.5.1", + "pinia": "^2.3.1", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.3", + "typescript": "~5.3.3", + "vite": "^5.0.11", + "vue-tsc": "^1.8.27" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.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/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz", + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.4" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.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/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz", + "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..aeb6936 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.5", + "element-plus": "^2.5.1", + "pinia": "^2.3.1", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.3", + "typescript": "~5.3.3", + "vite": "^5.0.11", + "vue-tsc": "^1.8.27" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..1038cd2 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,63 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..f187c2d --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,216 @@ +import axios from 'axios' +import type { + User, Token, LoginForm, DashboardStats, + Laboratory, Device, Reservation, Experiment, + Attendance, Maintenance, Notification +} from '../types' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000 +}) + +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token') + localStorage.removeItem('user') + window.location.href = '/login' + } + return Promise.reject(error) + } +) + +export const authApi = { + login: (data: LoginForm) => + api.post('/auth/login', new URLSearchParams({ + username: data.username, + password: data.password + }), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }), + + getCurrentUser: () => + api.get('/auth/me'), + + register: (data: any) => + api.post('/auth/register', data) +} + +export const userApi = { + getUsers: (params?: { skip?: number; limit?: number; role?: string; is_active?: boolean }) => + api.get('/users/', { params }), + + getUser: (id: number) => + api.get(`/users/${id}`), + + createUser: (data: any) => + api.post('/users/', data), + + updateUser: (id: number, data: any) => + api.put(`/users/${id}`, data), + + deleteUser: (id: number) => + api.delete(`/users/${id}`) +} + +export const labApi = { + getLaboratories: (params?: { skip?: number; limit?: number; status?: string }) => + api.get('/laboratories/', { params }), + + getLaboratory: (id: number) => + api.get(`/laboratories/${id}`), + + createLaboratory: (data: any) => + api.post('/laboratories/', data), + + updateLaboratory: (id: number, data: any) => + api.put(`/laboratories/${id}`, data), + + deleteLaboratory: (id: number) => + api.delete(`/laboratories/${id}`) +} + +export const deviceApi = { + getDevices: (params?: { skip?: number; limit?: number; status?: string; type?: string; laboratory_id?: number }) => + api.get('/devices/', { params }), + + getDevice: (id: number) => + api.get(`/devices/${id}`), + + createDevice: (data: any) => + api.post('/devices/', data), + + updateDevice: (id: number, data: any) => + api.put(`/devices/${id}`, data), + + deleteDevice: (id: number) => + api.delete(`/devices/${id}`) +} + +export const reservationApi = { + getReservations: (params?: { skip?: number; limit?: number; status?: string; user_id?: number; laboratory_id?: number }) => + api.get('/reservations/', { params }), + + getMyReservations: (params?: { skip?: number; limit?: number; status?: string }) => + api.get('/reservations/my', { params }), + + getReservation: (id: number) => + api.get(`/reservations/${id}`), + + createReservation: (data: any) => + api.post('/reservations/', data), + + updateReservation: (id: number, data: any) => + api.put(`/reservations/${id}`, data), + + deleteReservation: (id: number) => + api.delete(`/reservations/${id}`) +} + +export const experimentApi = { + getExperiments: (params?: { skip?: number; limit?: number; status?: string; user_id?: number; laboratory_id?: number }) => + api.get('/experiments/', { params }), + + getMyExperiments: (params?: { skip?: number; limit?: number; status?: string }) => + api.get('/experiments/my', { params }), + + getExperiment: (id: number) => + api.get(`/experiments/${id}`), + + createExperiment: (data: any) => + api.post('/experiments/', data), + + updateExperiment: (id: number, data: any) => + api.put(`/experiments/${id}`, data), + + deleteExperiment: (id: number) => + api.delete(`/experiments/${id}`) +} + +export const attendanceApi = { + getAttendances: (params?: { skip?: number; limit?: number; user_id?: number; date_from?: string; date_to?: string }) => + api.get('/attendances/', { params }), + + getMyAttendances: (params?: { skip?: number; limit?: number; date_from?: string; date_to?: string }) => + api.get('/attendances/my', { params }), + + getTodayAttendance: () => + api.get('/attendances/today'), + + checkIn: () => + api.post('/attendances/check-in'), + + checkOut: () => + api.post('/attendances/check-out'), + + getAttendance: (id: number) => + api.get(`/attendances/${id}`) +} + +export const maintenanceApi = { + getMaintenances: (params?: { skip?: number; limit?: number; status?: string; type?: string; device_id?: number }) => + api.get('/maintenances/', { params }), + + getMaintenance: (id: number) => + api.get(`/maintenances/${id}`), + + createMaintenance: (data: any) => + api.post('/maintenances/', data), + + updateMaintenance: (id: number, data: any) => + api.put(`/maintenances/${id}`, data), + + deleteMaintenance: (id: number) => + api.delete(`/maintenances/${id}`) +} + +export const notificationApi = { + getNotifications: (params?: { skip?: number; limit?: number; is_read?: boolean; type?: string }) => + api.get('/notifications/', { params }), + + getUnreadCount: () => + api.get<{ count: number }>('/notifications/unread-count'), + + getNotification: (id: number) => + api.get(`/notifications/${id}`), + + markAsRead: (id: number) => + api.put(`/notifications/${id}/read`), + + markAllAsRead: () => + api.put('/notifications/read-all'), + + deleteNotification: (id: number) => + api.delete(`/notifications/${id}`) +} + +export const dashboardApi = { + getStats: () => + api.get('/dashboard/stats'), + + getRecentActivities: (limit?: number) => + api.get('/dashboard/recent-activities', { params: { limit } }), + + getMonthlyStats: () => + api.get('/dashboard/monthly-stats'), + + getDeviceDistribution: () => + api.get('/dashboard/device-distribution') +} + +export default api \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue new file mode 100644 index 0000000..be256a3 --- /dev/null +++ b/frontend/src/layouts/MainLayout.vue @@ -0,0 +1,329 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..bebb609 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,20 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) + +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..8f08180 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,113 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useUserStore } from '../stores' +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: () => import('../views/Login.vue'), + meta: { title: '登录', requiresAuth: false } + }, + { + path: '/', + component: () => import('../layouts/MainLayout.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Dashboard', + component: () => import('../views/Dashboard.vue'), + meta: { title: '仪表板' } + }, + { + path: 'users', + name: 'Users', + component: () => import('../views/users/UserList.vue'), + meta: { title: '用户管理', requiresAdmin: true } + }, + { + path: 'laboratories', + name: 'Laboratories', + component: () => import('../views/labs/LabList.vue'), + meta: { title: '实验室管理' } + }, + { + path: 'devices', + name: 'Devices', + component: () => import('../views/devices/DeviceList.vue'), + meta: { title: '设备管理' } + }, + { + path: 'reservations', + name: 'Reservations', + component: () => import('../views/reservations/ReservationList.vue'), + meta: { title: '预约管理' } + }, + { + path: 'experiments', + name: 'Experiments', + component: () => import('../views/experiments/ExperimentList.vue'), + meta: { title: '实验记录' } + }, + { + path: 'attendance', + name: 'Attendance', + component: () => import('../views/attendance/AttendanceRecord.vue'), + meta: { title: '考勤管理' } + }, + { + path: 'maintenance', + name: 'Maintenance', + component: () => import('../views/maintenance/MaintenanceList.vue'), + meta: { title: '维护管理', requiresTeacher: true } + }, + { + path: 'notifications', + name: 'Notifications', + component: () => import('../views/notifications/NotificationList.vue'), + meta: { title: '通知公告' } + }, + { + path: 'profile', + name: 'Profile', + component: () => import('../views/Profile.vue'), + meta: { title: '个人中心' } + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach(async (to, from, next) => { + const userStore = useUserStore() + + document.title = to.meta.title ? `${to.meta.title} - 研究生实验室管理系统` : '研究生实验室管理系统' + + if (!userStore.isLoggedIn && userStore.token) { + userStore.initFromStorage() + } + + if (to.meta.requiresAuth !== false && !userStore.isLoggedIn) { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + if (to.meta.requiresAdmin && !userStore.isAdmin) { + next({ name: 'Dashboard' }) + return + } + + if (to.meta.requiresTeacher && !userStore.isTeacher) { + next({ name: 'Dashboard' }) + return + } + + next() +}) + +export default router \ No newline at end of file diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts new file mode 100644 index 0000000..3002fac --- /dev/null +++ b/frontend/src/stores/index.ts @@ -0,0 +1,2 @@ +export { useUserStore } from './user' +export { useNotificationStore } from './notification' \ No newline at end of file diff --git a/frontend/src/stores/notification.ts b/frontend/src/stores/notification.ts new file mode 100644 index 0000000..a785f84 --- /dev/null +++ b/frontend/src/stores/notification.ts @@ -0,0 +1,53 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { notificationApi } from '../api' + +export const useNotificationStore = defineStore('notification', () => { + const unreadCount = ref(0) + const notifications = ref([]) + + const fetchUnreadCount = async () => { + try { + const response = await notificationApi.getUnreadCount() + unreadCount.value = response.data.count + } catch (error) { + console.error('获取未读通知数量失败:', error) + } + } + + const fetchNotifications = async () => { + try { + const response = await notificationApi.getNotifications({ limit: 20 }) + notifications.value = response.data + } catch (error) { + console.error('获取通知列表失败:', error) + } + } + + const markAsRead = async (id: number) => { + try { + await notificationApi.markAsRead(id) + await fetchUnreadCount() + } catch (error) { + console.error('标记已读失败:', error) + } + } + + const markAllAsRead = async () => { + try { + await notificationApi.markAllAsRead() + unreadCount.value = 0 + } catch (error) { + console.error('标记全部已读失败:', error) + } + } + + return { + unreadCount, + notifications, + fetchUnreadCount, + fetchNotifications, + markAsRead, + markAllAsRead + } +}) \ No newline at end of file diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..860c432 --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,76 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { User } from '../types' +import { authApi } from '../api' + +export const useUserStore = defineStore('user', () => { + const token = ref(localStorage.getItem('token')) + const userInfo = ref(null) + + const isLoggedIn = computed(() => !!token.value) + const isAdmin = computed(() => userInfo.value?.role === 'admin') + const isTeacher = computed(() => userInfo.value?.role === 'teacher' || userInfo.value?.role === 'admin') + const isStudent = computed(() => userInfo.value?.role === 'student') + + const setToken = (newToken: string) => { + token.value = newToken + localStorage.setItem('token', newToken) + } + + const setUser = (user: User) => { + userInfo.value = user + localStorage.setItem('user', JSON.stringify(user)) + } + + const login = async (username: string, password: string) => { + const response = await authApi.login({ username, password }) + setToken(response.data.access_token) + setUser(response.data.user) + return response.data + } + + const logout = () => { + token.value = null + userInfo.value = null + localStorage.removeItem('token') + localStorage.removeItem('user') + } + + const fetchUserInfo = async () => { + if (!token.value) return null + try { + const response = await authApi.getCurrentUser() + setUser(response.data) + return response.data + } catch (error) { + logout() + return null + } + } + + const initFromStorage = () => { + const storedUser = localStorage.getItem('user') + if (storedUser) { + try { + userInfo.value = JSON.parse(storedUser) + } catch { + userInfo.value = null + } + } + } + + return { + token, + userInfo, + isLoggedIn, + isAdmin, + isTeacher, + isStudent, + setToken, + setUser, + login, + logout, + fetchUserInfo, + initFromStorage + } +}) \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..b3482db --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,149 @@ +export interface User { + id: number + username: string + name: string + email?: string + phone?: string + role: string + student_id?: string + major?: string + grade?: string + advisor?: string + is_active: boolean + created_at: string + updated_at?: string +} + +export interface Laboratory { + id: number + name: string + code: string + location?: string + capacity: number + manager?: string + description?: string + status: string + created_at: string + updated_at?: string +} + +export interface Device { + id: number + name: string + code: string + type?: string + model?: string + brand?: string + serial_number?: string + purchase_date?: string + price?: number + status: string + location?: string + description?: string + maintenance_cycle: number + last_maintenance?: string + laboratory_id?: number + user_id?: number + created_at: string + updated_at?: string + laboratory?: Laboratory + user?: User +} + +export interface Reservation { + id: number + title: string + purpose?: string + start_time: string + end_time: string + status: string + remarks?: string + user_id: number + laboratory_id?: number + device_id?: number + created_at: string + updated_at?: string + user?: User + laboratory?: Laboratory + device?: Device +} + +export interface Experiment { + id: number + title: string + content?: string + purpose?: string + hypothesis?: string + procedure?: string + result?: string + conclusion?: string + status: string + start_date?: string + end_date?: string + user_id: number + laboratory_id?: number + created_at: string + updated_at?: string + user?: User + laboratory?: Laboratory +} + +export interface Attendance { + id: number + check_in_time: string + check_out_time?: string + duration?: number + date: string + user_id: number + created_at: string + updated_at?: string + user?: User +} + +export interface Maintenance { + id: number + title: string + description?: string + type: string + status: string + scheduled_date?: string + completed_date?: string + cost?: number + remarks?: string + device_id: number + created_at: string + updated_at?: string + device?: Device +} + +export interface Notification { + id: number + title: string + content?: string + type: string + is_read: boolean + user_id?: number + created_at: string +} + +export interface DashboardStats { + total_users: number + total_laboratories: number + total_devices: number + total_experiments: number + today_attendances: number + pending_reservations: number + active_devices: number + maintenance_devices: number +} + +export interface Token { + access_token: string + token_type: string + user: User +} + +export interface LoginForm { + username: string + password: string +} \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..c9b84a7 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,476 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..7970ea3 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..68d04db --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,243 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue new file mode 100644 index 0000000..1613f6d --- /dev/null +++ b/frontend/src/views/Profile.vue @@ -0,0 +1,353 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/attendance/AttendanceRecord.vue b/frontend/src/views/attendance/AttendanceRecord.vue new file mode 100644 index 0000000..a601846 --- /dev/null +++ b/frontend/src/views/attendance/AttendanceRecord.vue @@ -0,0 +1,479 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/devices/DeviceList.vue b/frontend/src/views/devices/DeviceList.vue new file mode 100644 index 0000000..8e26418 --- /dev/null +++ b/frontend/src/views/devices/DeviceList.vue @@ -0,0 +1,400 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/experiments/ExperimentList.vue b/frontend/src/views/experiments/ExperimentList.vue new file mode 100644 index 0000000..da83886 --- /dev/null +++ b/frontend/src/views/experiments/ExperimentList.vue @@ -0,0 +1,467 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/labs/LabList.vue b/frontend/src/views/labs/LabList.vue new file mode 100644 index 0000000..04cf786 --- /dev/null +++ b/frontend/src/views/labs/LabList.vue @@ -0,0 +1,357 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/maintenance/MaintenanceList.vue b/frontend/src/views/maintenance/MaintenanceList.vue new file mode 100644 index 0000000..a792aee --- /dev/null +++ b/frontend/src/views/maintenance/MaintenanceList.vue @@ -0,0 +1,412 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/notifications/NotificationList.vue b/frontend/src/views/notifications/NotificationList.vue new file mode 100644 index 0000000..4c895e3 --- /dev/null +++ b/frontend/src/views/notifications/NotificationList.vue @@ -0,0 +1,294 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/reservations/ReservationList.vue b/frontend/src/views/reservations/ReservationList.vue new file mode 100644 index 0000000..d72e62e --- /dev/null +++ b/frontend/src/views/reservations/ReservationList.vue @@ -0,0 +1,410 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/users/UserList.vue b/frontend/src/views/users/UserList.vue new file mode 100644 index 0000000..7061665 --- /dev/null +++ b/frontend/src/views/users/UserList.vue @@ -0,0 +1,462 @@ + + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..9e1abed --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e39bec9 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + } +}) \ No newline at end of file