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 @@
+
+
+
+
+
+
+
+
+
+
总用户数
+
{{ stats.total_users }}
+
+
+
+
+
+
+
+
+
+
+
+
+
实验室数量
+
{{ stats.total_laboratories }}
+
+
+
+
+
+
+
+
+
+
+
+
+
设备总数
+
{{ stats.total_devices }}
+
+ 在用: {{ stats.active_devices }}
+ 维护: {{ stats.maintenance_devices }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
今日考勤
+
{{ stats.today_attendances }}
+
+ 待审核预约: {{ stats.pending_reservations }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activity.title }}
+
+ {{ getStatusText(activity.status) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 预约实验室
+
+
+
+
+
+
+ 新建实验
+
+
+
+
+
+
+ 设备管理
+
+
+
+
+
+
+ 考勤记录
+
+
+
+
+
+
+
+
+
+
+
+
+
可用
+
{{ deviceDistribution.available }}
+
+
+
+
使用中
+
{{ deviceDistribution.in_use }}
+
+
+
+
维护中
+
{{ deviceDistribution.maintenance }}
+
+
+
+
不可用
+
{{ deviceDistribution.unavailable }}
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 记住密码
+
+
+
+
+ {{ loading ? '登录中...' : '登 录' }}
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+ {{ userStore.userInfo?.name?.charAt(0) }}
+
+
{{ userStore.userInfo?.name }}
+
+ {{ getRoleText(userStore.userInfo?.role || '') }}
+
+
+
+
+
+
+ 用户名
+ {{ userStore.userInfo?.username }}
+
+
+
+ 邮箱
+ {{ userStore.userInfo?.email || '-' }}
+
+
+
+
手机号
+
{{ userStore.userInfo?.phone || '-' }}
+
+
+
+ 学号
+ {{ userStore.userInfo?.student_id }}
+
+
+
+ 专业
+ {{ userStore.userInfo?.major }}
+
+
+
+ 年级
+ {{ userStore.userInfo?.grade }}
+
+
+
+ 导师
+ {{ userStore.userInfo?.advisor }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存修改
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改密码
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
签到时间
+
{{ formatTime(todayAttendance.check_in_time) }}
+
+
+
+
+
+
+
+
+
签退时间
+
{{ todayAttendance.check_out_time ? formatTime(todayAttendance.check_out_time) : '未签退' }}
+
+
+
+
+
今日工作时长
+
{{ todayAttendance.duration.toFixed(2) }} 小时
+
+
+
+
+
+
+
+
+ 签到
+
+
+
+ 签退
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(scope.row.check_in_time) }}
+
+
+
+
+ {{ scope.row.check_out_time ? formatTime(scope.row.check_out_time) : '-' }}
+
+
+
+
+
+ {{ scope.row.duration.toFixed(2) }}h
+
+ -
+
+
+
+
+
+ {{ scope.row.check_out_time ? '已完成' : '进行中' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
本月出勤天数
+
{{ monthlyStats.attendanceDays }}
+
+
+
+
+
+
+
+
+
+
+
+
本月总工时
+
{{ monthlyStats.totalHours }}
+
+
+
+
+
+
+
+
+
+
+
+
平均日工时
+
{{ monthlyStats.avgHours }}
+
+
+
+
+
+
+
+
+
+
+
+
连续出勤
+
{{ monthlyStats.streakDays }}天
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+ {{ getTypeText(scope.row.type) }}
+
+
+
+
+
+
+ ¥{{ scope.row.price?.toLocaleString() || '-' }}
+
+
+
+
+
+ {{ getStatusText(scope.row.status) }}
+
+
+
+
+
+
+ 详情
+ 编辑
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentDevice?.code }}
+ {{ currentDevice?.name }}
+ {{ getTypeText(currentDevice?.type) }}
+
+ {{ getStatusText(currentDevice?.status || '') }}
+
+ {{ currentDevice?.brand || '-' }}
+ {{ currentDevice?.model || '-' }}
+ {{ currentDevice?.price ? '¥' + currentDevice.price.toLocaleString() : '-' }}
+ {{ currentDevice?.serial_number || '-' }}
+ {{ currentDevice?.purchase_date || '-' }}
+ {{ currentDevice?.location || '-' }}
+ {{ currentDevice?.description || '-' }}
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ exp.user?.name || '-' }}
+
+
+
+ {{ exp.laboratory?.name || '-' }}
+
+
+
+ 实验目的:{{ exp.purpose }}
+
+
+ {{ exp.content }}
+
+
+ 开始: {{ exp.start_date }}
+ 结束: {{ exp.end_date }}
+
+
+ 详情
+ 编辑
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentExperiment.title }}
+
+ {{ getStatusText(currentExperiment.status || '') }}
+
+ {{ currentExperiment.user?.name || '-' }}
+ {{ currentExperiment.laboratory?.name || '-' }}
+ {{ currentExperiment.start_date || '-' }}
+ {{ currentExperiment.end_date || '-' }}
+
+
+ 实验详情
+
+
+
实验目的
+
{{ currentExperiment.purpose }}
+
+
+
实验假设
+
{{ currentExperiment.hypothesis }}
+
+
+
实验步骤
+
{{ currentExperiment.procedure }}
+
+
+
实验结果
+
{{ currentExperiment.result }}
+
+
+
实验结论
+
{{ currentExperiment.conclusion }}
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+ {{ lab.name }}
+
+
+
+ {{ lab.location || '暂无' }}
+
+
+
+ 负责人: {{ lab.manager || '暂无' }}
+
+
+
+ 容量: {{ lab.capacity }}人
+
+
+
+ {{ lab.description }}
+
+
+
+ 预约
+
+
+ 编辑
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+ {{ getTypeText(scope.row.type) }}
+
+
+
+
+
+ {{ scope.row.device?.name || '-' }}
+
+
+
+
+
+ {{ scope.row.completed_date || '-' }}
+
+
+
+
+ {{ scope.row.cost ? '¥' + scope.row.cost : '-' }}
+
+
+
+
+
+ {{ getStatusText(scope.row.status) }}
+
+
+
+
+
+ 编辑
+
+ 完成
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+ 全部已读
+
+
+ 未读: {{ notificationStore.unreadCount }}
+
+
+
+
+
+
+
+
+
+ {{ notification.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+
+
+ 开始
+ {{ formatDateTime(scope.row.start_time) }}
+
+
+ 结束
+ {{ formatDateTime(scope.row.end_time) }}
+
+
+
+
+
+
+ {{ getStatusText(scope.row.status) }}
+
+
+
+
+
+ {{ scope.row.user?.name || '-' }}
+
+
+
+
+
+ 通过
+
+
+ 拒绝
+
+
+ 取消
+
+ 编辑
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getRoleText(scope.row.role) }}
+
+
+
+
+
+
+
+
+ {{ scope.row.is_active ? '正常' : '禁用' }}
+
+
+
+
+
+ 编辑
+ 重置密码
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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