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..0471605 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,107 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from app.routers import items, auth, users, laboratories, devices, reservations, experiments, attendances, maintenances, notifications, dashboard +from app.routers import lab_profiles, teachers, research_directions, research_achievements, lab_members, team_activities +from app.routers import servers, compute_tasks, usage_records, uploads +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.include_router(lab_profiles.router, prefix="/api/lab-profiles", tags=["实验室简介"]) +app.include_router(teachers.router, prefix="/api/teachers", tags=["教师信息"]) +app.include_router(research_directions.router, prefix="/api/research-directions", tags=["研究方向"]) +app.include_router(research_achievements.router, prefix="/api/research-achievements", tags=["研究成果"]) +app.include_router(lab_members.router, prefix="/api/lab-members", tags=["实验室成员"]) +app.include_router(team_activities.router, prefix="/api/team-activities", tags=["团建活动"]) + +app.include_router(servers.router, prefix="/api/servers", tags=["服务器管理"]) +app.include_router(compute_tasks.router, prefix="/api/compute-tasks", tags=["计算任务"]) +app.include_router(usage_records.router, prefix="/api/usage-records", tags=["使用记录"]) +app.include_router(uploads.router, prefix="/api/upload", 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..c4608aa --- /dev/null +++ b/backend/app/models/models.py @@ -0,0 +1,473 @@ +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()) + +class LabProfile(Base): + __tablename__ = "lab_profiles" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + name_en = Column(String(200), nullable=True) + code = Column(String(50), unique=True, nullable=False, index=True) + logo_url = Column(String(500), nullable=True) + location = Column(String(200), nullable=True) + building = Column(String(100), nullable=True) + room = Column(String(100), nullable=True) + established_year = Column(Integer, nullable=True) + director = Column(String(100), nullable=True) + director_title = Column(String(100), nullable=True) + contact_email = Column(String(100), nullable=True) + contact_phone = Column(String(50), nullable=True) + introduction = Column(Text, nullable=True) + mission = Column(Text, nullable=True) + vision = Column(Text, nullable=True) + history = Column(Text, nullable=True) + facilities = Column(Text, nullable=True) + achievements_overview = Column(Text, nullable=True) + is_active = Column(Boolean, default=True) + sort_order = Column(Integer, default=0) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + teachers = relationship("Teacher", back_populates="lab_profile") + research_directions = relationship("ResearchDirection", back_populates="lab_profile") + achievements = relationship("ResearchAchievement", back_populates="lab_profile") + members = relationship("LabMember", back_populates="lab_profile") + team_activities = relationship("TeamActivity", back_populates="lab_profile") + +class Teacher(Base): + __tablename__ = "teachers" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + name_en = Column(String(100), nullable=True) + avatar_url = Column(String(500), nullable=True) + title = Column(String(100), nullable=True) + position = Column(String(100), nullable=True) + email = Column(String(100), nullable=True) + phone = Column(String(50), nullable=True) + office = Column(String(100), nullable=True) + research_interests = Column(Text, nullable=True) + education = Column(Text, nullable=True) + experience = Column(Text, nullable=True) + publications = Column(Text, nullable=True) + projects = Column(Text, nullable=True) + honors = Column(Text, nullable=True) + personal_website = Column(String(500), nullable=True) + lab_profile_id = Column(Integer, ForeignKey("lab_profiles.id"), nullable=True) + sort_order = Column(Integer, default=0) + 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()) + + lab_profile = relationship("LabProfile", back_populates="teachers") + +class ResearchDirection(Base): + __tablename__ = "research_directions" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + name_en = Column(String(200), nullable=True) + description = Column(Text, nullable=True) + icon = Column(String(100), nullable=True) + key_technologies = Column(Text, nullable=True) + applications = Column(Text, nullable=True) + lab_profile_id = Column(Integer, ForeignKey("lab_profiles.id"), nullable=True) + sort_order = Column(Integer, default=0) + 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()) + + lab_profile = relationship("LabProfile", back_populates="research_directions") + +class ResearchAchievement(Base): + __tablename__ = "research_achievements" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(500), nullable=False) + type = Column(String(50), nullable=False, default="paper") + authors = Column(String(500), nullable=True) + publication = Column(String(300), nullable=True) + publication_date = Column(Date, nullable=True) + journal = Column(String(300), nullable=True) + volume = Column(String(50), nullable=True) + issue = Column(String(50), nullable=True) + pages = Column(String(50), nullable=True) + doi = Column(String(200), nullable=True) + patent_number = Column(String(100), nullable=True) + project_number = Column(String(100), nullable=True) + funding_amount = Column(Float, nullable=True) + funding_source = Column(String(200), nullable=True) + abstract = Column(Text, nullable=True) + keywords = Column(String(500), nullable=True) + link_url = Column(String(500), nullable=True) + google_scholar_url = Column(String(500), nullable=True) + citations = Column(Integer, default=0) + image_url = Column(String(500), nullable=True) + lab_profile_id = Column(Integer, ForeignKey("lab_profiles.id"), nullable=True) + sort_order = Column(Integer, default=0) + 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()) + + lab_profile = relationship("LabProfile", back_populates="achievements") + +class LabMember(Base): + __tablename__ = "lab_members" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + name = Column(String(100), nullable=False) + student_id = Column(String(50), nullable=True) + avatar_url = Column(String(500), nullable=True) + degree = Column(String(50), nullable=True) + grade = Column(String(50), nullable=True) + major = Column(String(100), nullable=True) + advisor = Column(String(100), nullable=True) + advisor_id = Column(Integer, ForeignKey("teachers.id"), nullable=True) + research_topic = Column(String(500), nullable=True) + bio = Column(Text, nullable=True) + email = Column(String(100), nullable=True) + phone = Column(String(50), nullable=True) + enrollment_date = Column(Date, nullable=True) + graduation_date = Column(Date, nullable=True) + status = Column(String(50), default="active") + lab_profile_id = Column(Integer, ForeignKey("lab_profiles.id"), nullable=True) + sort_order = Column(Integer, default=0) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + lab_profile = relationship("LabProfile", back_populates="members") + user = relationship("User") + advisor_teacher = relationship("Teacher") + +class TeamActivity(Base): + __tablename__ = "team_activities" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(200), nullable=False) + description = Column(Text, nullable=True) + activity_date = Column(Date, nullable=True) + location = Column(String(200), nullable=True) + participants = Column(String(500), nullable=True) + image_urls = Column(Text, nullable=True) + lab_profile_id = Column(Integer, ForeignKey("lab_profiles.id"), nullable=True) + sort_order = Column(Integer, default=0) + 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()) + + lab_profile = relationship("LabProfile", back_populates="team_activities") + +class Server(Base): + __tablename__ = "servers" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + code = Column(String(50), unique=True, nullable=False, index=True) + hostname = Column(String(100), nullable=True) + ip_address = Column(String(50), nullable=True) + ssh_port = Column(Integer, default=22) + ssh_user = Column(String(50), nullable=True) + ssh_password = Column(String(255), nullable=True) + ssh_key_path = Column(String(500), nullable=True) + location = Column(String(200), nullable=True) + rack = Column(String(50), nullable=True) + model = Column(String(200), nullable=True) + brand = Column(String(100), nullable=True) + purchase_date = Column(Date, nullable=True) + warranty_expiry = Column(Date, nullable=True) + description = Column(Text, nullable=True) + status = Column(String(50), default="available") + is_monitored = Column(Boolean, default=True) + last_checked_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + resources = relationship("ServerResource", back_populates="server", uselist=False) + tasks = relationship("ComputeTask", back_populates="server") + usage_records = relationship("UsageRecord", back_populates="server") + +class ServerResource(Base): + __tablename__ = "server_resources" + + id = Column(Integer, primary_key=True, index=True) + server_id = Column(Integer, ForeignKey("servers.id"), nullable=False, unique=True) + total_cpu_cores = Column(Integer, default=0) + used_cpu_cores = Column(Integer, default=0) + total_cpu_sockets = Column(Integer, default=1) + cpu_model = Column(String(200), nullable=True) + cpu_frequency = Column(String(50), nullable=True) + total_memory_gb = Column(Float, default=0) + used_memory_gb = Column(Float, default=0) + total_storage_tb = Column(Float, default=0) + used_storage_tb = Column(Float, default=0) + storage_type = Column(String(50), nullable=True) + total_gpus = Column(Integer, default=0) + used_gpus = Column(Integer, default=0) + gpu_model = Column(String(200), nullable=True) + gpu_memory_gb = Column(Float, default=0) + network_bandwidth_gbps = Column(Float, default=1) + power_consumption_watts = Column(Integer, nullable=True) + cpu_usage_percent = Column(Float, default=0) + memory_usage_percent = Column(Float, default=0) + gpu_usage_percent = Column(Float, default=0) + disk_usage_percent = Column(Float, default=0) + uptime_seconds = Column(Integer, default=0) + load_average = Column(String(100), nullable=True) + last_updated = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + server = relationship("Server", back_populates="resources") + +class ComputeTask(Base): + __tablename__ = "compute_tasks" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + task_id = Column(String(100), unique=True, nullable=True, index=True) + description = Column(Text, nullable=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + server_id = Column(Integer, ForeignKey("servers.id"), nullable=True) + priority = Column(Integer, default=5) + required_cpu_cores = Column(Integer, default=1) + required_memory_gb = Column(Float, default=1) + required_gpus = Column(Integer, default=0) + required_storage_gpu_memory_gb = Column(Float, default=0) + estimated_duration_hours = Column(Float, nullable=True) + workspace_path = Column(String(500), nullable=True) + script_path = Column(String(500), nullable=True) + command = Column(Text, nullable=True) + environment = Column(Text, nullable=True) + container_image = Column(String(200), nullable=True) + output_path = Column(String(500), nullable=True) + log_path = Column(String(500), nullable=True) + status = Column(String(50), default="pending") + progress = Column(Integer, default=0) + queue_position = Column(Integer, nullable=True) + pid = Column(Integer, nullable=True) + submit_time = Column(DateTime(timezone=True), nullable=True) + start_time = Column(DateTime(timezone=True), nullable=True) + end_time = Column(DateTime(timezone=True), nullable=True) + actual_duration_seconds = Column(Integer, nullable=True) + exit_code = Column(Integer, nullable=True) + error_message = Column(Text, nullable=True) + result_summary = Column(Text, nullable=True) + allocated_cpu_cores = Column(Integer, default=0) + allocated_memory_gb = Column(Float, default=0) + allocated_gpus = Column(Integer, default=0) + peak_cpu_usage_percent = Column(Float, nullable=True) + peak_memory_usage_gb = Column(Float, nullable=True) + gpu_usage = Column(Text, nullable=True) + is_public = Column(Boolean, default=False) + parent_task_id = Column(Integer, ForeignKey("compute_tasks.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") + server = relationship("Server", back_populates="tasks") + parent_task = relationship("ComputeTask", remote_side=[id]) + +class UsageRecord(Base): + __tablename__ = "usage_records" + + id = Column(Integer, primary_key=True, index=True) + server_id = Column(Integer, ForeignKey("servers.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + task_id = Column(Integer, ForeignKey("compute_tasks.id"), nullable=True) + record_date = Column(Date, nullable=False, index=True) + cpu_usage_hours = Column(Float, default=0) + gpu_usage_hours = Column(Float, default=0) + memory_usage_gb_hours = Column(Float, default=0) + storage_usage_tb_hours = Column(Float, default=0) + network_upload_gb = Column(Float, default=0) + network_download_gb = Column(Float, default=0) + peak_cpu_percent = Column(Float, nullable=True) + peak_memory_percent = Column(Float, nullable=True) + peak_gpu_percent = Column(Float, nullable=True) + cost_estimate = Column(Float, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + server = relationship("Server", back_populates="usage_records") + user = relationship("User") + task = relationship("ComputeTask") \ 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..64fad89 --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,117 @@ +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, PasswordChange +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="邮箱已被注册") + + if len(user_data.password) < 6: + raise HTTPException(status_code=400, detail="密码长度不能少于6位") + + 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="student", + 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 + +@router.put("/me/change-password") +def change_password( + password_data: PasswordChange, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if not verify_password(password_data.old_password, current_user.password): + raise HTTPException(status_code=400, detail="当前密码错误") + + if len(password_data.new_password) < 6: + raise HTTPException(status_code=400, detail="新密码长度不能少于6位") + + current_user.password = get_password_hash(password_data.new_password) + db.commit() + return {"message": "密码修改成功"} \ No newline at end of file diff --git a/backend/app/routers/compute_tasks.py b/backend/app/routers/compute_tasks.py new file mode 100644 index 0000000..5766933 --- /dev/null +++ b/backend/app/routers/compute_tasks.py @@ -0,0 +1,278 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy.orm import joinedload +from typing import List, Optional +from datetime import datetime +from app.database import get_db +from app.models.models import ComputeTask, Server, ServerResource +from app.schemas.schemas import ( + ComputeTask as ComputeTaskSchema, + ComputeTaskCreate, + ComputeTaskUpdate, + TaskDetail +) +from app.routers.auth import get_current_active_user +from app.models.models import User +import uuid + +router = APIRouter() + +@router.get("/", response_model=List[TaskDetail]) +def read_tasks( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + server_id: Optional[int] = None, + user_id: Optional[int] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ) + + if status: + query = query.filter(ComputeTask.status == status) + if server_id: + query = query.filter(ComputeTask.server_id == server_id) + if user_id and (current_user.role == "admin" or current_user.role == "teacher"): + query = query.filter(ComputeTask.user_id == user_id) + elif user_id is None and current_user.role == "student": + query = query.filter(ComputeTask.user_id == current_user.id) + + tasks = query.order_by(ComputeTask.submit_time.desc(), ComputeTask.id).offset(skip).limit(limit).all() + return tasks + +@router.get("/my", response_model=List[TaskDetail]) +def read_my_tasks( + 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(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ).filter(ComputeTask.user_id == current_user.id) + + if status: + query = query.filter(ComputeTask.status == status) + + tasks = query.order_by(ComputeTask.submit_time.desc(), ComputeTask.id).offset(skip).limit(limit).all() + return tasks + +@router.get("/{task_id}", response_model=TaskDetail) +def read_task( + task_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task = db.query(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ).filter(ComputeTask.id == task_id).first() + + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if current_user.role == "student" and task.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return task + +@router.post("/", response_model=TaskDetail) +def create_task( + task_data: ComputeTaskCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task_dict = task_data.dict() + task_dict["user_id"] = current_user.id + task_dict["status"] = "pending" + task_dict["task_id"] = f"task_{uuid.uuid4().hex[:8]}" + task_dict["submit_time"] = datetime.now() + + new_task = ComputeTask(**task_dict) + db.add(new_task) + db.commit() + db.refresh(new_task) + + task = db.query(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ).filter(ComputeTask.id == new_task.id).first() + + return task + +@router.post("/submit", response_model=TaskDetail) +def submit_task( + task_data: ComputeTaskCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task_dict = task_data.dict() + task_dict["user_id"] = current_user.id + task_dict["task_id"] = f"task_{uuid.uuid4().hex[:8]}" + task_dict["submit_time"] = datetime.now() + + available_servers = db.query(Server).filter( + Server.status == "available" + ).all() + + if not available_servers: + task_dict["status"] = "queued" + else: + selected_server = None + for server in available_servers: + resources = db.query(ServerResource).filter( + ServerResource.server_id == server.id + ).first() + if resources: + available_cpu = resources.total_cpu_cores - resources.used_cpu_cores + available_memory = resources.total_memory_gb - resources.used_memory_gb + available_gpus = resources.total_gpus - resources.used_gpus + + if (available_cpu >= task_data.required_cpu_cores and + available_memory >= task_data.required_memory_gb and + available_gpus >= task_data.required_gpus): + selected_server = server + break + + if selected_server: + task_dict["server_id"] = selected_server.id + task_dict["status"] = "running" + task_dict["start_time"] = datetime.now() + task_dict["allocated_cpu_cores"] = task_data.required_cpu_cores + task_dict["allocated_memory_gb"] = task_data.required_memory_gb + task_dict["allocated_gpus"] = task_data.required_gpus + else: + task_dict["status"] = "queued" + + new_task = ComputeTask(**task_dict) + db.add(new_task) + db.commit() + db.refresh(new_task) + + task = db.query(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ).filter(ComputeTask.id == new_task.id).first() + + return task + +@router.put("/{task_id}", response_model=TaskDetail) +def update_task( + task_id: int, + task_data: ComputeTaskUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task = db.query(ComputeTask).filter(ComputeTask.id == task_id).first() + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if current_user.role == "student" and task.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + if task.status not in ["pending", "queued"]: + raise HTTPException(status_code=400, detail="任务已开始执行,无法修改") + + update_data = task_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(task, key, value) + + db.commit() + db.refresh(task) + + task_detail = db.query(ComputeTask).options( + joinedload(ComputeTask.user), + joinedload(ComputeTask.server) + ).filter(ComputeTask.id == task.id).first() + + return task_detail + +@router.post("/{task_id}/cancel") +def cancel_task( + task_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task = db.query(ComputeTask).filter(ComputeTask.id == task_id).first() + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if current_user.role == "student" and task.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + if task.status not in ["pending", "queued"]: + raise HTTPException(status_code=400, detail="任务已开始执行,无法取消") + + task.status = "cancelled" + task.end_time = datetime.now() + db.commit() + + return {"message": "任务已取消"} + +@router.post("/{task_id}/pause") +def pause_task( + task_id: int, + 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="权限不足") + + task = db.query(ComputeTask).filter(ComputeTask.id == task_id).first() + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if task.status != "running": + raise HTTPException(status_code=400, detail="只有运行中的任务可以暂停") + + task.status = "paused" + db.commit() + + return {"message": "任务已暂停"} + +@router.post("/{task_id}/resume") +def resume_task( + task_id: int, + 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="权限不足") + + task = db.query(ComputeTask).filter(ComputeTask.id == task_id).first() + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if task.status != "paused": + raise HTTPException(status_code=400, detail="只有暂停的任务可以恢复") + + task.status = "running" + db.commit() + + return {"message": "任务已恢复"} + +@router.delete("/{task_id}") +def delete_task( + task_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + task = db.query(ComputeTask).filter(ComputeTask.id == task_id).first() + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + + if current_user.role != "admin": + if task.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + if task.status in ["running", "paused"]: + raise HTTPException(status_code=400, detail="运行中的任务无法删除") + + db.delete(task) + db.commit() + return {"message": "任务已删除"} 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/lab_members.py b/backend/app/routers/lab_members.py new file mode 100644 index 0000000..0b6966a --- /dev/null +++ b/backend/app/routers/lab_members.py @@ -0,0 +1,99 @@ +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 LabMember +from app.schemas.schemas import ( + LabMember as LabMemberSchema, + LabMemberCreate, + LabMemberUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[LabMemberSchema]) +def read_lab_members( + skip: int = 0, + limit: int = 100, + lab_profile_id: Optional[int] = None, + status: Optional[str] = None, + degree: Optional[str] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(LabMember) + if lab_profile_id: + query = query.filter(LabMember.lab_profile_id == lab_profile_id) + if status: + query = query.filter(LabMember.status == status) + if degree: + query = query.filter(LabMember.degree == degree) + members = query.order_by(LabMember.sort_order, LabMember.id).offset(skip).limit(limit).all() + return members + +@router.get("/{member_id}", response_model=LabMemberSchema) +def read_lab_member( + member_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + member = db.query(LabMember).filter(LabMember.id == member_id).first() + if member is None: + raise HTTPException(status_code=404, detail="实验室成员不存在") + return member + +@router.post("/", response_model=LabMemberSchema) +def create_lab_member( + member_data: LabMemberCreate, + 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_member = LabMember(**member_data.dict()) + db.add(new_member) + db.commit() + db.refresh(new_member) + return new_member + +@router.put("/{member_id}", response_model=LabMemberSchema) +def update_lab_member( + member_id: int, + member_data: LabMemberUpdate, + 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="权限不足") + + member = db.query(LabMember).filter(LabMember.id == member_id).first() + if member is None: + raise HTTPException(status_code=404, detail="实验室成员不存在") + + update_data = member_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(member, key, value) + + db.commit() + db.refresh(member) + return member + +@router.delete("/{member_id}") +def delete_lab_member( + member_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="权限不足") + + member = db.query(LabMember).filter(LabMember.id == member_id).first() + if member is None: + raise HTTPException(status_code=404, detail="实验室成员不存在") + + db.delete(member) + db.commit() + return {"message": "实验室成员已删除"} diff --git a/backend/app/routers/lab_profiles.py b/backend/app/routers/lab_profiles.py new file mode 100644 index 0000000..b9e9788 --- /dev/null +++ b/backend/app/routers/lab_profiles.py @@ -0,0 +1,114 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy.orm import joinedload +from typing import List, Optional +from app.database import get_db +from app.models.models import LabProfile, Teacher, ResearchDirection, ResearchAchievement, LabMember, TeamActivity +from app.schemas.schemas import ( + LabProfile as LabProfileSchema, + LabProfileCreate, + LabProfileUpdate, + LabProfileDetail +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[LabProfileSchema]) +def read_lab_profiles( + skip: int = 0, + limit: int = 100, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(LabProfile) + if is_active is not None: + query = query.filter(LabProfile.is_active == is_active) + lab_profiles = query.order_by(LabProfile.sort_order, LabProfile.id).offset(skip).limit(limit).all() + return lab_profiles + +@router.get("/{profile_id}", response_model=LabProfileDetail) +def read_lab_profile( + profile_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + lab_profile = db.query(LabProfile).options( + joinedload(LabProfile.teachers), + joinedload(LabProfile.research_directions), + joinedload(LabProfile.achievements), + joinedload(LabProfile.members), + joinedload(LabProfile.team_activities) + ).filter(LabProfile.id == profile_id).first() + if lab_profile is None: + raise HTTPException(status_code=404, detail="实验室简介不存在") + return lab_profile + +@router.post("/", response_model=LabProfileSchema) +def create_lab_profile( + profile_data: LabProfileCreate, + 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 = db.query(LabProfile).filter(LabProfile.code == profile_data.code).first() + if existing: + raise HTTPException(status_code=400, detail="实验室编号已存在") + + new_profile = LabProfile(**profile_data.dict()) + db.add(new_profile) + db.commit() + db.refresh(new_profile) + return new_profile + +@router.put("/{profile_id}", response_model=LabProfileSchema) +def update_lab_profile( + profile_id: int, + profile_data: LabProfileUpdate, + 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="权限不足") + + lab_profile = db.query(LabProfile).filter(LabProfile.id == profile_id).first() + if lab_profile is None: + raise HTTPException(status_code=404, detail="实验室简介不存在") + + update_data = profile_data.dict(exclude_unset=True) + + if "code" in update_data: + existing = db.query(LabProfile).filter( + LabProfile.code == update_data["code"], + LabProfile.id != profile_id + ).first() + if existing: + raise HTTPException(status_code=400, detail="实验室编号已存在") + + for key, value in update_data.items(): + setattr(lab_profile, key, value) + + db.commit() + db.refresh(lab_profile) + return lab_profile + +@router.delete("/{profile_id}") +def delete_lab_profile( + profile_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="权限不足") + + lab_profile = db.query(LabProfile).filter(LabProfile.id == profile_id).first() + if lab_profile is None: + raise HTTPException(status_code=404, detail="实验室简介不存在") + + db.delete(lab_profile) + db.commit() + return {"message": "实验室简介已删除"} 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/research_achievements.py b/backend/app/routers/research_achievements.py new file mode 100644 index 0000000..3ca63db --- /dev/null +++ b/backend/app/routers/research_achievements.py @@ -0,0 +1,99 @@ +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 ResearchAchievement +from app.schemas.schemas import ( + ResearchAchievement as ResearchAchievementSchema, + ResearchAchievementCreate, + ResearchAchievementUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[ResearchAchievementSchema]) +def read_achievements( + skip: int = 0, + limit: int = 100, + lab_profile_id: Optional[int] = None, + type: Optional[str] = None, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(ResearchAchievement) + if lab_profile_id: + query = query.filter(ResearchAchievement.lab_profile_id == lab_profile_id) + if type: + query = query.filter(ResearchAchievement.type == type) + if is_active is not None: + query = query.filter(ResearchAchievement.is_active == is_active) + achievements = query.order_by(ResearchAchievement.sort_order, ResearchAchievement.publication_date.desc(), ResearchAchievement.id).offset(skip).limit(limit).all() + return achievements + +@router.get("/{achievement_id}", response_model=ResearchAchievementSchema) +def read_achievement( + achievement_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + achievement = db.query(ResearchAchievement).filter(ResearchAchievement.id == achievement_id).first() + if achievement is None: + raise HTTPException(status_code=404, detail="研究成果不存在") + return achievement + +@router.post("/", response_model=ResearchAchievementSchema) +def create_achievement( + achievement_data: ResearchAchievementCreate, + 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_achievement = ResearchAchievement(**achievement_data.dict()) + db.add(new_achievement) + db.commit() + db.refresh(new_achievement) + return new_achievement + +@router.put("/{achievement_id}", response_model=ResearchAchievementSchema) +def update_achievement( + achievement_id: int, + achievement_data: ResearchAchievementUpdate, + 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="权限不足") + + achievement = db.query(ResearchAchievement).filter(ResearchAchievement.id == achievement_id).first() + if achievement is None: + raise HTTPException(status_code=404, detail="研究成果不存在") + + update_data = achievement_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(achievement, key, value) + + db.commit() + db.refresh(achievement) + return achievement + +@router.delete("/{achievement_id}") +def delete_achievement( + achievement_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="权限不足") + + achievement = db.query(ResearchAchievement).filter(ResearchAchievement.id == achievement_id).first() + if achievement is None: + raise HTTPException(status_code=404, detail="研究成果不存在") + + db.delete(achievement) + db.commit() + return {"message": "研究成果已删除"} diff --git a/backend/app/routers/research_directions.py b/backend/app/routers/research_directions.py new file mode 100644 index 0000000..f63ca02 --- /dev/null +++ b/backend/app/routers/research_directions.py @@ -0,0 +1,96 @@ +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 ResearchDirection +from app.schemas.schemas import ( + ResearchDirection as ResearchDirectionSchema, + ResearchDirectionCreate, + ResearchDirectionUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[ResearchDirectionSchema]) +def read_research_directions( + skip: int = 0, + limit: int = 100, + lab_profile_id: Optional[int] = None, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(ResearchDirection) + if lab_profile_id: + query = query.filter(ResearchDirection.lab_profile_id == lab_profile_id) + if is_active is not None: + query = query.filter(ResearchDirection.is_active == is_active) + directions = query.order_by(ResearchDirection.sort_order, ResearchDirection.id).offset(skip).limit(limit).all() + return directions + +@router.get("/{direction_id}", response_model=ResearchDirectionSchema) +def read_research_direction( + direction_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + direction = db.query(ResearchDirection).filter(ResearchDirection.id == direction_id).first() + if direction is None: + raise HTTPException(status_code=404, detail="研究方向不存在") + return direction + +@router.post("/", response_model=ResearchDirectionSchema) +def create_research_direction( + direction_data: ResearchDirectionCreate, + 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_direction = ResearchDirection(**direction_data.dict()) + db.add(new_direction) + db.commit() + db.refresh(new_direction) + return new_direction + +@router.put("/{direction_id}", response_model=ResearchDirectionSchema) +def update_research_direction( + direction_id: int, + direction_data: ResearchDirectionUpdate, + 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="权限不足") + + direction = db.query(ResearchDirection).filter(ResearchDirection.id == direction_id).first() + if direction is None: + raise HTTPException(status_code=404, detail="研究方向不存在") + + update_data = direction_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(direction, key, value) + + db.commit() + db.refresh(direction) + return direction + +@router.delete("/{direction_id}") +def delete_research_direction( + direction_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="权限不足") + + direction = db.query(ResearchDirection).filter(ResearchDirection.id == direction_id).first() + if direction is None: + raise HTTPException(status_code=404, detail="研究方向不存在") + + db.delete(direction) + db.commit() + return {"message": "研究方向已删除"} 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/servers.py b/backend/app/routers/servers.py new file mode 100644 index 0000000..0bea881 --- /dev/null +++ b/backend/app/routers/servers.py @@ -0,0 +1,162 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy.orm import joinedload +from typing import List, Optional +from app.database import get_db +from app.models.models import Server, ServerResource +from app.schemas.schemas import ( + Server as ServerSchema, + ServerCreate, + ServerUpdate, + ServerDetail, + ServerResource as ServerResourceSchema, + ServerResourceCreate, + ServerResourceUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[ServerSchema]) +def read_servers( + skip: int = 0, + limit: int = 100, + status: Optional[str] = None, + is_monitored: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Server) + if status: + query = query.filter(Server.status == status) + if is_monitored is not None: + query = query.filter(Server.is_monitored == is_monitored) + servers = query.order_by(Server.id).offset(skip).limit(limit).all() + return servers + +@router.get("/{server_id}", response_model=ServerDetail) +def read_server( + server_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + server = db.query(Server).options( + joinedload(Server.resources) + ).filter(Server.id == server_id).first() + if server is None: + raise HTTPException(status_code=404, detail="服务器不存在") + return server + +@router.post("/", response_model=ServerSchema) +def create_server( + server_data: ServerCreate, + 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 = db.query(Server).filter(Server.code == server_data.code).first() + if existing: + raise HTTPException(status_code=400, detail="服务器编号已存在") + + new_server = Server(**server_data.dict()) + db.add(new_server) + db.commit() + db.refresh(new_server) + + default_resource = ServerResource( + server_id=new_server.id + ) + db.add(default_resource) + db.commit() + + return new_server + +@router.put("/{server_id}", response_model=ServerSchema) +def update_server( + server_id: int, + server_data: ServerUpdate, + 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="权限不足") + + server = db.query(Server).filter(Server.id == server_id).first() + if server is None: + raise HTTPException(status_code=404, detail="服务器不存在") + + update_data = server_data.dict(exclude_unset=True) + + if "code" in update_data: + existing = db.query(Server).filter( + Server.code == update_data["code"], + Server.id != server_id + ).first() + if existing: + raise HTTPException(status_code=400, detail="服务器编号已存在") + + for key, value in update_data.items(): + setattr(server, key, value) + + db.commit() + db.refresh(server) + return server + +@router.delete("/{server_id}") +def delete_server( + server_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="权限不足") + + server = db.query(Server).filter(Server.id == server_id).first() + if server is None: + raise HTTPException(status_code=404, detail="服务器不存在") + + db.delete(server) + db.commit() + return {"message": "服务器已删除"} + +@router.get("/{server_id}/resources", response_model=ServerResourceSchema) +def get_server_resources( + server_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + resources = db.query(ServerResource).filter(ServerResource.server_id == server_id).first() + if resources is None: + raise HTTPException(status_code=404, detail="服务器资源信息不存在") + return resources + +@router.put("/{server_id}/resources", response_model=ServerResourceSchema) +def update_server_resources( + server_id: int, + resource_data: ServerResourceUpdate, + 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="权限不足") + + resources = db.query(ServerResource).filter(ServerResource.server_id == server_id).first() + if resources is None: + server = db.query(Server).filter(Server.id == server_id).first() + if server is None: + raise HTTPException(status_code=404, detail="服务器不存在") + resources = ServerResource(server_id=server_id) + db.add(resources) + db.commit() + db.refresh(resources) + + update_data = resource_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(resources, key, value) + + db.commit() + db.refresh(resources) + return resources diff --git a/backend/app/routers/teachers.py b/backend/app/routers/teachers.py new file mode 100644 index 0000000..aa831ce --- /dev/null +++ b/backend/app/routers/teachers.py @@ -0,0 +1,96 @@ +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 Teacher +from app.schemas.schemas import ( + Teacher as TeacherSchema, + TeacherCreate, + TeacherUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[TeacherSchema]) +def read_teachers( + skip: int = 0, + limit: int = 100, + lab_profile_id: Optional[int] = None, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(Teacher) + if lab_profile_id: + query = query.filter(Teacher.lab_profile_id == lab_profile_id) + if is_active is not None: + query = query.filter(Teacher.is_active == is_active) + teachers = query.order_by(Teacher.sort_order, Teacher.id).offset(skip).limit(limit).all() + return teachers + +@router.get("/{teacher_id}", response_model=TeacherSchema) +def read_teacher( + teacher_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + teacher = db.query(Teacher).filter(Teacher.id == teacher_id).first() + if teacher is None: + raise HTTPException(status_code=404, detail="教师不存在") + return teacher + +@router.post("/", response_model=TeacherSchema) +def create_teacher( + teacher_data: TeacherCreate, + 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_teacher = Teacher(**teacher_data.dict()) + db.add(new_teacher) + db.commit() + db.refresh(new_teacher) + return new_teacher + +@router.put("/{teacher_id}", response_model=TeacherSchema) +def update_teacher( + teacher_id: int, + teacher_data: TeacherUpdate, + 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="权限不足") + + teacher = db.query(Teacher).filter(Teacher.id == teacher_id).first() + if teacher is None: + raise HTTPException(status_code=404, detail="教师不存在") + + update_data = teacher_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(teacher, key, value) + + db.commit() + db.refresh(teacher) + return teacher + +@router.delete("/{teacher_id}") +def delete_teacher( + teacher_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="权限不足") + + teacher = db.query(Teacher).filter(Teacher.id == teacher_id).first() + if teacher is None: + raise HTTPException(status_code=404, detail="教师不存在") + + db.delete(teacher) + db.commit() + return {"message": "教师已删除"} diff --git a/backend/app/routers/team_activities.py b/backend/app/routers/team_activities.py new file mode 100644 index 0000000..b712a97 --- /dev/null +++ b/backend/app/routers/team_activities.py @@ -0,0 +1,96 @@ +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 TeamActivity +from app.schemas.schemas import ( + TeamActivity as TeamActivitySchema, + TeamActivityCreate, + TeamActivityUpdate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[TeamActivitySchema]) +def read_team_activities( + skip: int = 0, + limit: int = 100, + lab_profile_id: Optional[int] = None, + is_active: Optional[bool] = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + query = db.query(TeamActivity) + if lab_profile_id: + query = query.filter(TeamActivity.lab_profile_id == lab_profile_id) + if is_active is not None: + query = query.filter(TeamActivity.is_active == is_active) + activities = query.order_by(TeamActivity.activity_date.desc(), TeamActivity.sort_order, TeamActivity.id).offset(skip).limit(limit).all() + return activities + +@router.get("/{activity_id}", response_model=TeamActivitySchema) +def read_team_activity( + activity_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + activity = db.query(TeamActivity).filter(TeamActivity.id == activity_id).first() + if activity is None: + raise HTTPException(status_code=404, detail="团建活动不存在") + return activity + +@router.post("/", response_model=TeamActivitySchema) +def create_team_activity( + activity_data: TeamActivityCreate, + 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_activity = TeamActivity(**activity_data.dict()) + db.add(new_activity) + db.commit() + db.refresh(new_activity) + return new_activity + +@router.put("/{activity_id}", response_model=TeamActivitySchema) +def update_team_activity( + activity_id: int, + activity_data: TeamActivityUpdate, + 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="权限不足") + + activity = db.query(TeamActivity).filter(TeamActivity.id == activity_id).first() + if activity is None: + raise HTTPException(status_code=404, detail="团建活动不存在") + + update_data = activity_data.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(activity, key, value) + + db.commit() + db.refresh(activity) + return activity + +@router.delete("/{activity_id}") +def delete_team_activity( + activity_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="权限不足") + + activity = db.query(TeamActivity).filter(TeamActivity.id == activity_id).first() + if activity is None: + raise HTTPException(status_code=404, detail="团建活动不存在") + + db.delete(activity) + db.commit() + return {"message": "团建活动已删除"} diff --git a/backend/app/routers/uploads.py b/backend/app/routers/uploads.py new file mode 100644 index 0000000..73fdd57 --- /dev/null +++ b/backend/app/routers/uploads.py @@ -0,0 +1,95 @@ +from fastapi import APIRouter, UploadFile, File, Depends, HTTPException +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from typing import Optional +import os +import uuid +from datetime import datetime +from pathlib import Path + +from app.database import get_db +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +UPLOAD_DIR = Path("uploads") +UPLOAD_DIR.mkdir(exist_ok=True) + +ALLOWED_IMAGE_TYPES = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp" +] + +MAX_FILE_SIZE = 5 * 1024 * 1024 + +def get_file_extension(filename: str) -> str: + return os.path.splitext(filename)[1].lower() + +def generate_unique_filename(filename: str) -> str: + ext = get_file_extension(filename) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + unique_id = uuid.uuid4().hex[:8] + return f"{timestamp}_{unique_id}{ext}" + +@router.post("/") +async def upload_file( + file: UploadFile = File(...), + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + if not file.content_type: + raise HTTPException(status_code=400, detail="无法识别文件类型") + + if file.content_type not in ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=400, + detail=f"不支持的文件类型。支持的类型: {', '.join(ALLOWED_IMAGE_TYPES)}" + ) + + file_content = await file.read() + + if len(file_content) > MAX_FILE_SIZE: + raise HTTPException( + status_code=400, + detail=f"文件大小超过限制。最大允许: {MAX_FILE_SIZE / 1024 / 1024}MB" + ) + + filename = generate_unique_filename(file.filename or "unknown.jpg") + file_path = UPLOAD_DIR / filename + + with open(file_path, "wb") as f: + f.write(file_content) + + return { + "filename": filename, + "url": f"/api/upload/{filename}", + "size": len(file_content), + "content_type": file.content_type + } + +@router.get("/{filename}") +async def get_uploaded_file(filename: str): + file_path = UPLOAD_DIR / filename + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + + content_type = "application/octet-stream" + ext = get_file_extension(filename) + if ext in [".jpg", ".jpeg"]: + content_type = "image/jpeg" + elif ext == ".png": + content_type = "image/png" + elif ext == ".gif": + content_type = "image/gif" + elif ext == ".webp": + content_type = "image/webp" + + return FileResponse( + path=str(file_path), + media_type=content_type + ) diff --git a/backend/app/routers/usage_records.py b/backend/app/routers/usage_records.py new file mode 100644 index 0000000..31a8303 --- /dev/null +++ b/backend/app/routers/usage_records.py @@ -0,0 +1,171 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy.orm import joinedload +from typing import List, Optional +from datetime import date +from app.database import get_db +from app.models.models import UsageRecord +from app.schemas.schemas import ( + UsageRecord as UsageRecordSchema, + UsageRecordCreate +) +from app.routers.auth import get_current_active_user +from app.models.models import User + +router = APIRouter() + +@router.get("/", response_model=List[UsageRecordSchema]) +def read_usage_records( + skip: int = 0, + limit: int = 100, + server_id: Optional[int] = None, + user_id: Optional[int] = None, + task_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(UsageRecord).options( + joinedload(UsageRecord.server), + joinedload(UsageRecord.user), + joinedload(UsageRecord.task) + ) + + if server_id: + query = query.filter(UsageRecord.server_id == server_id) + if task_id: + query = query.filter(UsageRecord.task_id == task_id) + + if user_id and (current_user.role == "admin" or current_user.role == "teacher"): + query = query.filter(UsageRecord.user_id == user_id) + elif user_id is None and current_user.role == "student": + query = query.filter(UsageRecord.user_id == current_user.id) + + if date_from: + query = query.filter(UsageRecord.record_date >= date_from) + if date_to: + query = query.filter(UsageRecord.record_date <= date_to) + + records = query.order_by(UsageRecord.record_date.desc(), UsageRecord.id).offset(skip).limit(limit).all() + return records + +@router.get("/my", response_model=List[UsageRecordSchema]) +def read_my_usage_records( + skip: int = 0, + limit: int = 100, + server_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(UsageRecord).options( + joinedload(UsageRecord.server), + joinedload(UsageRecord.user), + joinedload(UsageRecord.task) + ).filter(UsageRecord.user_id == current_user.id) + + if server_id: + query = query.filter(UsageRecord.server_id == server_id) + if date_from: + query = query.filter(UsageRecord.record_date >= date_from) + if date_to: + query = query.filter(UsageRecord.record_date <= date_to) + + records = query.order_by(UsageRecord.record_date.desc(), UsageRecord.id).offset(skip).limit(limit).all() + return records + +@router.get("/{record_id}", response_model=UsageRecordSchema) +def read_usage_record( + record_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db) +): + record = db.query(UsageRecord).options( + joinedload(UsageRecord.server), + joinedload(UsageRecord.user), + joinedload(UsageRecord.task) + ).filter(UsageRecord.id == record_id).first() + + if record is None: + raise HTTPException(status_code=404, detail="使用记录不存在") + + if current_user.role == "student" and record.user_id != current_user.id: + raise HTTPException(status_code=403, detail="权限不足") + + return record + +@router.get("/stats/summary") +def get_usage_summary( + server_id: Optional[int] = None, + 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) +): + from sqlalchemy import func + + query = db.query( + func.sum(UsageRecord.cpu_usage_hours).label("total_cpu_hours"), + func.sum(UsageRecord.gpu_usage_hours).label("total_gpu_hours"), + func.sum(UsageRecord.memory_usage_gb_hours).label("total_memory_gb_hours"), + func.sum(UsageRecord.storage_usage_tb_hours).label("total_storage_tb_hours"), + func.sum(UsageRecord.network_upload_gb).label("total_upload_gb"), + func.sum(UsageRecord.network_download_gb).label("total_download_gb"), + func.count(UsageRecord.id).label("total_records"), + func.avg(UsageRecord.peak_cpu_percent).label("avg_cpu_percent"), + func.avg(UsageRecord.peak_memory_percent).label("avg_memory_percent"), + func.avg(UsageRecord.peak_gpu_percent).label("avg_gpu_percent") + ) + + if server_id: + query = query.filter(UsageRecord.server_id == server_id) + + if user_id and (current_user.role == "admin" or current_user.role == "teacher"): + query = query.filter(UsageRecord.user_id == user_id) + elif user_id is None and current_user.role == "student": + query = query.filter(UsageRecord.user_id == current_user.id) + + if date_from: + query = query.filter(UsageRecord.record_date >= date_from) + if date_to: + query = query.filter(UsageRecord.record_date <= date_to) + + result = query.first() + + return { + "total_cpu_hours": float(result.total_cpu_hours or 0), + "total_gpu_hours": float(result.total_gpu_hours or 0), + "total_memory_gb_hours": float(result.total_memory_gb_hours or 0), + "total_storage_tb_hours": float(result.total_storage_tb_hours or 0), + "total_upload_gb": float(result.total_upload_gb or 0), + "total_download_gb": float(result.total_download_gb or 0), + "total_records": int(result.total_records or 0), + "avg_cpu_percent": float(result.avg_cpu_percent or 0), + "avg_memory_percent": float(result.avg_memory_percent or 0), + "avg_gpu_percent": float(result.avg_gpu_percent or 0) + } + +@router.post("/", response_model=UsageRecordSchema) +def create_usage_record( + record_data: UsageRecordCreate, + 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_record = UsageRecord(**record_data.dict()) + db.add(new_record) + db.commit() + db.refresh(new_record) + + record = db.query(UsageRecord).options( + joinedload(UsageRecord.server), + joinedload(UsageRecord.user), + joinedload(UsageRecord.task) + ).filter(UsageRecord.id == new_record.id).first() + + return record diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py new file mode 100644 index 0000000..eda71c4 --- /dev/null +++ b/backend/app/routers/users.py @@ -0,0 +1,114 @@ +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) + + if "password" in update_data and update_data["password"]: + if len(update_data["password"]) < 6: + raise HTTPException(status_code=400, detail="密码长度不能少于6位") + update_data["password"] = get_password_hash(update_data["password"]) + + 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..ad158e5 --- /dev/null +++ b/backend/app/schemas/schemas.py @@ -0,0 +1,855 @@ +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 + password: Optional[str] = None + +class PasswordChange(BaseModel): + old_password: str + new_password: str + +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 + +class AchievementType(str, Enum): + PAPER = "paper" + PATENT = "patent" + PROJECT = "project" + AWARD = "award" + SOFTWARE = "software" + OTHER = "other" + +class TaskStatus(str, Enum): + PENDING = "pending" + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + PAUSED = "paused" + +class ServerStatus(str, Enum): + AVAILABLE = "available" + BUSY = "busy" + MAINTENANCE = "maintenance" + OFFLINE = "offline" + ERROR = "error" + +class LabProfileBase(BaseModel): + name: str + name_en: Optional[str] = None + code: str + logo_url: Optional[str] = None + location: Optional[str] = None + building: Optional[str] = None + room: Optional[str] = None + established_year: Optional[int] = None + director: Optional[str] = None + director_title: Optional[str] = None + contact_email: Optional[str] = None + contact_phone: Optional[str] = None + introduction: Optional[str] = None + mission: Optional[str] = None + vision: Optional[str] = None + history: Optional[str] = None + facilities: Optional[str] = None + achievements_overview: Optional[str] = None + is_active: bool = True + sort_order: int = 0 + +class LabProfileCreate(LabProfileBase): + pass + +class LabProfileUpdate(BaseModel): + name: Optional[str] = None + name_en: Optional[str] = None + code: Optional[str] = None + logo_url: Optional[str] = None + location: Optional[str] = None + building: Optional[str] = None + room: Optional[str] = None + established_year: Optional[int] = None + director: Optional[str] = None + director_title: Optional[str] = None + contact_email: Optional[str] = None + contact_phone: Optional[str] = None + introduction: Optional[str] = None + mission: Optional[str] = None + vision: Optional[str] = None + history: Optional[str] = None + facilities: Optional[str] = None + achievements_overview: Optional[str] = None + is_active: Optional[bool] = None + sort_order: Optional[int] = None + +class LabProfile(LabProfileBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class TeacherBase(BaseModel): + name: str + name_en: Optional[str] = None + avatar_url: Optional[str] = None + title: Optional[str] = None + position: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + office: Optional[str] = None + research_interests: Optional[str] = None + education: Optional[str] = None + experience: Optional[str] = None + publications: Optional[str] = None + projects: Optional[str] = None + honors: Optional[str] = None + personal_website: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: int = 0 + is_active: bool = True + +class TeacherCreate(TeacherBase): + pass + +class TeacherUpdate(BaseModel): + name: Optional[str] = None + name_en: Optional[str] = None + avatar_url: Optional[str] = None + title: Optional[str] = None + position: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + office: Optional[str] = None + research_interests: Optional[str] = None + education: Optional[str] = None + experience: Optional[str] = None + publications: Optional[str] = None + projects: Optional[str] = None + honors: Optional[str] = None + personal_website: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + +class Teacher(TeacherBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class ResearchDirectionBase(BaseModel): + name: str + name_en: Optional[str] = None + description: Optional[str] = None + icon: Optional[str] = None + key_technologies: Optional[str] = None + applications: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: int = 0 + is_active: bool = True + +class ResearchDirectionCreate(ResearchDirectionBase): + pass + +class ResearchDirectionUpdate(BaseModel): + name: Optional[str] = None + name_en: Optional[str] = None + description: Optional[str] = None + icon: Optional[str] = None + key_technologies: Optional[str] = None + applications: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + +class ResearchDirection(ResearchDirectionBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class ResearchAchievementBase(BaseModel): + title: str + type: str = "paper" + authors: Optional[str] = None + publication: Optional[str] = None + publication_date: Optional[date] = None + journal: Optional[str] = None + volume: Optional[str] = None + issue: Optional[str] = None + pages: Optional[str] = None + doi: Optional[str] = None + patent_number: Optional[str] = None + project_number: Optional[str] = None + funding_amount: Optional[float] = None + funding_source: Optional[str] = None + abstract: Optional[str] = None + keywords: Optional[str] = None + link_url: Optional[str] = None + google_scholar_url: Optional[str] = None + citations: Optional[int] = 0 + image_url: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: int = 0 + is_active: bool = True + +class ResearchAchievementCreate(ResearchAchievementBase): + pass + +class ResearchAchievementUpdate(BaseModel): + title: Optional[str] = None + type: Optional[str] = None + authors: Optional[str] = None + publication: Optional[str] = None + publication_date: Optional[date] = None + journal: Optional[str] = None + volume: Optional[str] = None + issue: Optional[str] = None + pages: Optional[str] = None + doi: Optional[str] = None + patent_number: Optional[str] = None + project_number: Optional[str] = None + funding_amount: Optional[float] = None + funding_source: Optional[str] = None + abstract: Optional[str] = None + keywords: Optional[str] = None + link_url: Optional[str] = None + google_scholar_url: Optional[str] = None + citations: Optional[int] = None + image_url: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + +class ResearchAchievement(ResearchAchievementBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class LabMemberBase(BaseModel): + user_id: Optional[int] = None + name: str + student_id: Optional[str] = None + avatar_url: Optional[str] = None + degree: Optional[str] = None + grade: Optional[str] = None + major: Optional[str] = None + advisor: Optional[str] = None + advisor_id: Optional[int] = None + research_topic: Optional[str] = None + bio: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + enrollment_date: Optional[date] = None + graduation_date: Optional[date] = None + status: str = "active" + lab_profile_id: Optional[int] = None + sort_order: int = 0 + +class LabMemberCreate(LabMemberBase): + pass + +class LabMemberUpdate(BaseModel): + user_id: Optional[int] = None + name: Optional[str] = None + student_id: Optional[str] = None + avatar_url: Optional[str] = None + degree: Optional[str] = None + grade: Optional[str] = None + major: Optional[str] = None + advisor: Optional[str] = None + advisor_id: Optional[int] = None + research_topic: Optional[str] = None + bio: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + enrollment_date: Optional[date] = None + graduation_date: Optional[date] = None + status: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: Optional[int] = None + +class LabMember(LabMemberBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class TeamActivityBase(BaseModel): + title: str + description: Optional[str] = None + activity_date: Optional[date] = None + location: Optional[str] = None + participants: Optional[str] = None + image_urls: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: int = 0 + is_active: bool = True + +class TeamActivityCreate(TeamActivityBase): + pass + +class TeamActivityUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + activity_date: Optional[date] = None + location: Optional[str] = None + participants: Optional[str] = None + image_urls: Optional[str] = None + lab_profile_id: Optional[int] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + +class TeamActivity(TeamActivityBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class ServerBase(BaseModel): + name: str + code: str + hostname: Optional[str] = None + ip_address: Optional[str] = None + ssh_port: int = 22 + ssh_user: Optional[str] = None + ssh_password: Optional[str] = None + ssh_key_path: Optional[str] = None + location: Optional[str] = None + rack: Optional[str] = None + model: Optional[str] = None + brand: Optional[str] = None + purchase_date: Optional[date] = None + warranty_expiry: Optional[date] = None + description: Optional[str] = None + status: str = "available" + is_monitored: bool = True + +class ServerCreate(ServerBase): + pass + +class ServerUpdate(BaseModel): + name: Optional[str] = None + code: Optional[str] = None + hostname: Optional[str] = None + ip_address: Optional[str] = None + ssh_port: Optional[int] = None + ssh_user: Optional[str] = None + ssh_password: Optional[str] = None + ssh_key_path: Optional[str] = None + location: Optional[str] = None + rack: Optional[str] = None + model: Optional[str] = None + brand: Optional[str] = None + purchase_date: Optional[date] = None + warranty_expiry: Optional[date] = None + description: Optional[str] = None + status: Optional[str] = None + is_monitored: Optional[bool] = None + +class Server(ServerBase): + id: int + last_checked_at: Optional[datetime] = None + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class ServerResourceBase(BaseModel): + server_id: int + total_cpu_cores: int = 0 + used_cpu_cores: int = 0 + total_cpu_sockets: int = 1 + cpu_model: Optional[str] = None + cpu_frequency: Optional[str] = None + total_memory_gb: float = 0.0 + used_memory_gb: float = 0.0 + total_storage_tb: float = 0.0 + used_storage_tb: float = 0.0 + storage_type: Optional[str] = None + total_gpus: int = 0 + used_gpus: int = 0 + gpu_model: Optional[str] = None + gpu_memory_gb: float = 0.0 + network_bandwidth_gbps: float = 1.0 + power_consumption_watts: Optional[int] = None + cpu_usage_percent: float = 0.0 + memory_usage_percent: float = 0.0 + gpu_usage_percent: float = 0.0 + disk_usage_percent: float = 0.0 + uptime_seconds: int = 0 + load_average: Optional[str] = None + +class ServerResourceCreate(ServerResourceBase): + pass + +class ServerResourceUpdate(BaseModel): + total_cpu_cores: Optional[int] = None + used_cpu_cores: Optional[int] = None + total_cpu_sockets: Optional[int] = None + cpu_model: Optional[str] = None + cpu_frequency: Optional[str] = None + total_memory_gb: Optional[float] = None + used_memory_gb: Optional[float] = None + total_storage_tb: Optional[float] = None + used_storage_tb: Optional[float] = None + storage_type: Optional[str] = None + total_gpus: Optional[int] = None + used_gpus: Optional[int] = None + gpu_model: Optional[str] = None + gpu_memory_gb: Optional[float] = None + network_bandwidth_gbps: Optional[float] = None + power_consumption_watts: Optional[int] = None + cpu_usage_percent: Optional[float] = None + memory_usage_percent: Optional[float] = None + gpu_usage_percent: Optional[float] = None + disk_usage_percent: Optional[float] = None + uptime_seconds: Optional[int] = None + load_average: Optional[str] = None + +class ServerResource(ServerResourceBase): + id: int + last_updated: Optional[datetime] = None + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class ComputeTaskBase(BaseModel): + name: str + task_id: Optional[str] = None + description: Optional[str] = None + priority: int = 5 + required_cpu_cores: int = 1 + required_memory_gb: float = 1.0 + required_gpus: int = 0 + required_storage_gpu_memory_gb: float = 0.0 + estimated_duration_hours: Optional[float] = None + workspace_path: Optional[str] = None + script_path: Optional[str] = None + command: Optional[str] = None + environment: Optional[str] = None + container_image: Optional[str] = None + output_path: Optional[str] = None + log_path: Optional[str] = None + is_public: bool = False + parent_task_id: Optional[int] = None + +class ComputeTaskCreate(ComputeTaskBase): + pass + +class ComputeTaskUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + priority: Optional[int] = None + status: Optional[str] = None + progress: Optional[int] = None + output_path: Optional[str] = None + log_path: Optional[str] = None + is_public: Optional[bool] = None + +class ComputeTask(ComputeTaskBase): + id: int + user_id: int + server_id: Optional[int] = None + status: str = "pending" + progress: int = 0 + queue_position: Optional[int] = None + pid: Optional[int] = None + submit_time: Optional[datetime] = None + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + actual_duration_seconds: Optional[int] = None + exit_code: Optional[int] = None + error_message: Optional[str] = None + result_summary: Optional[str] = None + allocated_cpu_cores: int = 0 + allocated_memory_gb: float = 0.0 + allocated_gpus: int = 0 + peak_cpu_usage_percent: Optional[float] = None + peak_memory_usage_gb: Optional[float] = None + gpu_usage: Optional[str] = None + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class UsageRecordBase(BaseModel): + server_id: int + user_id: Optional[int] = None + task_id: Optional[int] = None + record_date: date + cpu_usage_hours: float = 0.0 + gpu_usage_hours: float = 0.0 + memory_usage_gb_hours: float = 0.0 + storage_usage_tb_hours: float = 0.0 + network_upload_gb: float = 0.0 + network_download_gb: float = 0.0 + peak_cpu_percent: Optional[float] = None + peak_memory_percent: Optional[float] = None + peak_gpu_percent: Optional[float] = None + cost_estimate: Optional[float] = None + +class UsageRecordCreate(UsageRecordBase): + pass + +class UsageRecord(UsageRecordBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + +class LabProfileDetail(LabProfile): + teachers: List[Teacher] = [] + research_directions: List[ResearchDirection] = [] + achievements: List[ResearchAchievement] = [] + members: List[LabMember] = [] + team_activities: List[TeamActivity] = [] + +class ServerDetail(Server): + resources: Optional[ServerResource] = None + +class TaskDetail(ComputeTask): + user: Optional[User] = None + server: Optional[Server] = None \ 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..d974852 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..2bc8fe2 --- /dev/null +++ b/backend/app/utils/auth.py @@ -0,0 +1,61 @@ +from datetime import datetime, timedelta +from typing import Optional +import hashlib +import bcrypt +from jose import JWTError, jwt +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() + +BCRYPT_ROUNDS = 12 + +def _hash_password_sha256(password: str) -> str: + return hashlib.sha256(password.encode('utf-8')).hexdigest() + +def _ensure_utf8_bytes(s: str) -> bytes: + return s.encode('utf-8') + +def verify_password(plain_password: str, hashed_password: str) -> bool: + try: + sha256_password = _hash_password_sha256(plain_password) + password_bytes = _ensure_utf8_bytes(sha256_password) + hashed_bytes = _ensure_utf8_bytes(hashed_password) + return bcrypt.checkpw(password_bytes, hashed_bytes) + except Exception: + try: + password_bytes = _ensure_utf8_bytes(plain_password) + if len(password_bytes) > 72: + password_bytes = password_bytes[:72] + hashed_bytes = _ensure_utf8_bytes(hashed_password) + return bcrypt.checkpw(password_bytes, hashed_bytes) + except Exception: + return False + +def get_password_hash(password: str) -> str: + sha256_password = _hash_password_sha256(password) + password_bytes = _ensure_utf8_bytes(sha256_password) + salt = bcrypt.gensalt(rounds=BCRYPT_ROUNDS) + hashed = bcrypt.hashpw(password_bytes, salt) + return hashed.decode('utf-8') + +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 diff --git a/backend/init_sample_data.py b/backend/init_sample_data.py new file mode 100644 index 0000000..ee2ed43 --- /dev/null +++ b/backend/init_sample_data.py @@ -0,0 +1,656 @@ +from sqlalchemy.orm import Session +from app.database import SessionLocal, engine +from app.models.models import ( + Base, LabProfile, Teacher, ResearchDirection, ResearchAchievement, + LabMember, TeamActivity, Server, ServerResource, ComputeTask, User +) +from datetime import date, datetime +from app.utils.auth import get_password_hash + +Base.metadata.create_all(bind=engine) + +def init_sample_data(): + db: Session = SessionLocal() + try: + print("正在初始化示例数据...") + + lab_profile = db.query(LabProfile).filter(LabProfile.code == "AILAB").first() + if not lab_profile: + lab_profile = LabProfile( + name="人工智能与机器学习实验室", + name_en="Artificial Intelligence and Machine Learning Lab", + code="AILAB", + location="计算机科学与技术学院", + building="科研楼", + room="A座401-408", + established_year=2010, + director="张明教授", + director_title="实验室主任、博士生导师", + contact_email="ailab@university.edu.cn", + contact_phone="010-88888888", + introduction="""人工智能与机器学习实验室成立于2010年,是计算机科学与技术学院下属的重点实验室之一。实验室致力于人工智能、机器学习、深度学习、计算机视觉、自然语言处理等前沿领域的研究。 + +实验室拥有一支高水平的研究团队,包括教授5人、副教授8人、讲师10人,以及在读博士研究生30余人、硕士研究生60余人。实验室近年来承担了多项国家级和省部级科研项目,包括国家重点研发计划、国家自然科学基金重点项目等。 + +实验室配备了先进的计算设备,包括高性能GPU服务器集群、大数据处理平台等,为科研工作提供了强有力的支撑。""", + mission="以国家人工智能发展战略为导向,致力于人工智能与机器学习领域的基础研究和应用技术研发,培养高水平的人工智能专业人才,为推动人工智能技术的发展和应用做出贡献。", + vision="建设成为国际知名、国内领先的人工智能研究机构,在深度学习、计算机视觉、自然语言处理等方向取得具有国际影响力的研究成果,培养一批具有创新精神和实践能力的人工智能人才。", + history="""2010年:实验室正式成立,初期研究方向聚焦于机器学习和数据挖掘 +2013年:获批为省部级重点实验室 +2015年:开始深度学习方向的研究,建立第一台GPU服务器集群 +2018年:研究成果在顶级会议发表,获得多项重要奖项 +2020年:实验室扩建,科研团队和设备规模进一步扩大 +2023年:获批国家级重点实验室培育基地""", + facilities="""高性能计算服务器: +- 10台GPU服务器(NVIDIA A100 80GB × 8 / 服务器) +- 5台GPU服务器(NVIDIA V100 32GB × 8 / 服务器) +- CPU计算节点:20台,总核心数超过1000核 + +存储系统: +- 分布式存储系统,总容量超过500TB +- 高速缓存存储,支持大模型训练 + +网络设施: +- 100Gbps高速内网 +- 专用科研网络出口 + +软件平台: +- TensorFlow、PyTorch、MindSpore等深度学习框架 +- 自研模型训练和部署平台 +- 数据集管理系统""", + achievements_overview="实验室近年来取得了丰硕的研究成果,在NeurIPS、ICML、CVPR、ICCV、ACL、EMNLP等人工智能领域顶级会议发表论文100余篇,获得国家技术发明奖2项、省部级科技进步奖5项。授权发明专利50余项,软件著作权30余项。", + is_active=True, + sort_order=1 + ) + db.add(lab_profile) + db.commit() + db.refresh(lab_profile) + print("✓ 实验室简介已创建") + + teachers_data = [ + { + "name": "张明", + "name_en": "Ming Zhang", + "title": "教授", + "position": "实验室主任、博士生导师", + "email": "zhangming@university.edu.cn", + "phone": "010-88888801", + "office": "科研楼A座401室", + "research_interests": "深度学习、计算机视觉、模式识别", + "education": """博士:清华大学 计算机科学与技术 2005-2009 +硕士:北京大学 计算机应用技术 2002-2005 +学士:浙江大学 计算机科学与技术 1998-2002""", + "experience": """2020-至今:计算机科学与技术学院 教授、博士生导师 +2015-2020:计算机科学与技术学院 副教授、硕士生导师 +2012-2015:斯坦福大学 访问学者 +2009-2012:计算机科学与技术学院 讲师""", + "publications": """1. Zhang, M., et al. "Deep Learning for Visual Recognition." NeurIPS 2022. +2. Zhang, M., et al. "Efficient Transformer Architecture for Image Classification." CVPR 2021. +3. Zhang, M., et al. "Attention Mechanisms in Computer Vision: A Survey." IJCV 2020.""", + "projects": """1. 国家重点研发计划:面向复杂场景的视觉理解与推理(2022-2025) +2. 国家自然科学基金重点项目:深度学习理论与方法研究(2020-2024) +3. 科技部创新2030项目:新一代人工智能关键技术研究(2018-2022)""", + "honors": """1. 国家技术发明奖二等奖(2021) +2. 教育部自然科学奖一等奖(2020) +3. 国家优秀青年科学基金获得者(2016) +4. 教育部新世纪优秀人才支持计划(2014)""", + "personal_website": "https://ailab.university.edu.cn/zhangming", + "lab_profile_id": lab_profile.id, + "sort_order": 1 + }, + { + "name": "李华", + "name_en": "Hua Li", + "title": "教授", + "position": "博士生导师", + "email": "lihua@university.edu.cn", + "phone": "010-88888802", + "office": "科研楼A座402室", + "research_interests": "自然语言处理、大语言模型、知识图谱", + "education": """博士:中国科学院计算技术研究所 2006-2010 +硕士:中国科学技术大学 2003-2006 +学士:华中科技大学 1999-2003""", + "experience": """2018-至今:计算机科学与技术学院 教授、博士生导师 +2013-2018:计算机科学与技术学院 副教授 +2010-2013:微软亚洲研究院 副研究员""", + "publications": """1. Li, H., et al. "Large Language Models: A Comprehensive Survey." ACL 2023. +2. Li, H., et al. "Knowledge-Enhanced Pre-training for NLP." EMNLP 2022. +3. Li, H., et al. "Efficient Fine-tuning Strategies for LLMs." NeurIPS 2021.""", + "projects": """1. 国家自然科学基金面上项目:知识图谱驱动的自然语言理解(2021-2024) +2. 科技部重点项目:大语言模型关键技术研究(2022-2025)""", + "honors": """1. 教育部科技进步奖一等奖(2022) +2. 中国中文信息学会科学技术奖一等奖(2021) +3. 国家优秀青年科学基金获得者(2017)""", + "personal_website": "https://ailab.university.edu.cn/lihua", + "lab_profile_id": lab_profile.id, + "sort_order": 2 + }, + { + "name": "王强", + "name_en": "Qiang Wang", + "title": "副教授", + "position": "硕士生导师", + "email": "wangqiang@university.edu.cn", + "phone": "010-88888803", + "office": "科研楼A座403室", + "research_interests": "强化学习、多智能体系统、机器人学", + "education": """博士:香港科技大学 电子与计算机工程 2011-2015 +硕士:上海交通大学 自动化系 2008-2011 +学士:西安交通大学 自动化 2004-2008""", + "experience": """2019-至今:计算机科学与技术学院 副教授、硕士生导师 +2015-2019:计算机科学与技术学院 讲师""", + "publications": """1. Wang, Q., et al. "Multi-Agent Reinforcement Learning: A Survey." JMLR 2022. +2. Wang, Q., et al. "Efficient Exploration in Reinforcement Learning." ICML 2021.""", + "projects": """1. 国家自然科学基金青年项目:多智能体强化学习理论与方法(2020-2022) +2. 北京市自然科学基金:面向智能机器人的强化学习研究(2021-2023)""", + "honors": """1. 中国自动化学会优秀博士论文奖(2016) +2. 香江学者奖(2015)""", + "personal_website": "https://ailab.university.edu.cn/wangqiang", + "lab_profile_id": lab_profile.id, + "sort_order": 3 + } + ] + + for td in teachers_data: + teacher = db.query(Teacher).filter(Teacher.name == td["name"]).first() + if not teacher: + db.add(Teacher(**td)) + db.commit() + print("✓ 教师信息已创建") + + directions_data = [ + { + "name": "深度学习与计算机视觉", + "name_en": "Deep Learning and Computer Vision", + "description": "研究深度学习理论、卷积神经网络、视觉识别、目标检测、图像分割等方向。聚焦于视觉基础模型、视觉语言预训练、多模态学习等前沿课题。", + "icon": "Picture", + "key_technologies": "深度学习,卷积神经网络,Transformer,视觉基础模型,目标检测,图像分割,多模态学习", + "applications": "自动驾驶,医疗影像分析,智能监控,人脸识别,工业质检,遥感图像分析", + "lab_profile_id": lab_profile.id, + "sort_order": 1 + }, + { + "name": "自然语言处理与大语言模型", + "name_en": "Natural Language Processing and Large Language Models", + "description": "研究自然语言理解、机器翻译、文本生成、对话系统、知识图谱等方向。重点关注大语言模型(LLM)的理论基础、高效训练、对齐与安全等问题。", + "icon": "Document", + "key_technologies": "Transformer,BERT,GPT,大语言模型,预训练,指令微调,RLHF,知识图谱", + "applications": "智能客服,机器翻译,文本摘要,智能写作,代码生成,智能助手", + "lab_profile_id": lab_profile.id, + "sort_order": 2 + }, + { + "name": "强化学习与智能决策", + "name_en": "Reinforcement Learning and Intelligent Decision Making", + "description": "研究强化学习理论、多智能体系统、规划与决策等方向。探索在复杂环境下的自主学习与决策能力,应用于机器人、游戏、推荐系统等场景。", + "icon": "MagicStick", + "key_technologies": "深度强化学习,多智能体系统,马尔可夫决策过程,策略梯度,演员-评论家方法,模仿学习", + "applications": "机器人控制,游戏AI,推荐系统,自动驾驶决策,智能调度", + "lab_profile_id": lab_profile.id, + "sort_order": 3 + }, + { + "name": "机器学习与数据挖掘", + "name_en": "Machine Learning and Data Mining", + "description": "研究传统机器学习方法、特征工程、模型可解释性、因果推断等方向。专注于开发高效、可靠的机器学习算法,解决实际应用中的数据建模问题。", + "icon": "DataLine", + "key_technologies": "监督学习,无监督学习,半监督学习,集成学习,特征选择,可解释性AI,因果推断", + "applications": "金融风控,用户画像,异常检测,预测分析,推荐系统", + "lab_profile_id": lab_profile.id, + "sort_order": 4 + } + ] + + for dd in directions_data: + direction = db.query(ResearchDirection).filter(ResearchDirection.name == dd["name"]).first() + if not direction: + db.add(ResearchDirection(**dd)) + db.commit() + print("✓ 研究方向已创建") + + achievements_data = [ + { + "title": "Efficient Vision Transformers with Hierarchical Token Reduction", + "type": "paper", + "authors": "张明, 王小明, 李华", + "publication": "NeurIPS 2023", + "publication_date": date(2023, 12, 10), + "journal": "Advances in Neural Information Processing Systems", + "volume": "36", + "pages": "12345-12358", + "abstract": "视觉Transformer在图像识别任务中取得了显著成效,但其在处理高分辨率图像时的计算复杂度限制了其在实际应用中的部署。本文提出了一种层次化的Token约简机制,通过动态选择重要的Token来降低计算开销,同时保持模型性能。实验结果表明,本文方法在ImageNet基准上取得了与现有方法相当的精度,但计算复杂度降低了40%以上。", + "keywords": "Vision Transformer,Token Reduction,Efficient Inference,Image Classification", + "doi": "10.48550/arXiv.2311.xxxx", + "lab_profile_id": lab_profile.id, + "sort_order": 1 + }, + { + "title": "Knowledge-Enhanced Pre-training for Domain-Specific NLP", + "type": "paper", + "authors": "李华, 张伟, 刘芳", + "publication": "ACL 2023", + "publication_date": date(2023, 7, 9), + "journal": "Annual Meeting of the Association for Computational Linguistics", + "pages": "5678-5692", + "abstract": "领域特定的自然语言处理任务通常面临数据稀缺的问题。本文提出了一种知识增强的预训练框架,通过将领域知识图谱融入预训练过程,显著提升了模型在下游任务中的表现。在医疗、法律等多个领域的实验结果表明,本文方法相比传统预训练方法在少样本和零样本场景下取得了显著提升。", + "keywords": "Knowledge Graph,Pre-training,Domain Adaptation,Few-shot Learning", + "doi": "10.48550/arXiv.2305.xxxx", + "lab_profile_id": lab_profile.id, + "sort_order": 2 + }, + { + "title": "Multi-Agent Coordination with Communication-Aware Reinforcement Learning", + "type": "paper", + "authors": "王强, 赵阳, 陈晨", + "publication": "ICML 2023", + "publication_date": date(2023, 7, 23), + "journal": "International Conference on Machine Learning", + "pages": "34567-34580", + "abstract": "多智能体协同是人工智能领域的核心问题之一。本文提出了一种通信感知的强化学习方法,通过学习智能体间的有效通信策略来提升协同性能。与传统方法不同,本文的框架能够自适应地决定何时通信以及通信什么内容。在多个多智能体基准环境中的实验验证了本文方法的有效性。", + "keywords": "Multi-Agent Reinforcement Learning,Communication,Coordination", + "doi": "10.48550/arXiv.2306.xxxx", + "lab_profile_id": lab_profile.id, + "sort_order": 3 + }, + { + "title": "一种用于图像识别的轻量化Transformer网络架构", + "type": "patent", + "authors": "张明, 王小明", + "patent_number": "CN202310234567.8", + "publication_date": date(2023, 3, 15), + "abstract": "本发明公开了一种用于图像识别的轻量化Transformer网络架构,属于计算机视觉技术领域。所述架构包括:特征提取模块、层次化注意力模块、特征聚合模块。本发明通过设计高效的注意力机制和特征约简策略,在保持识别精度的同时大幅降低了模型参数量和计算开销,特别适合部署在移动端和边缘设备上。", + "keywords": "Transformer,轻量化网络,图像识别,边缘部署", + "lab_profile_id": lab_profile.id, + "sort_order": 4 + }, + { + "title": "面向复杂场景的视觉理解与推理", + "type": "project", + "authors": "张明, 李华, 王强", + "project_number": "2022YFBXXXXX", + "funding_amount": 800.0, + "funding_source": "国家重点研发计划", + "publication_date": date(2022, 10, 1), + "abstract": "本项目针对复杂场景下的视觉理解与推理问题,研究视觉基础模型、多模态学习、因果推理等核心技术。目标是构建能够理解复杂场景、进行逻辑推理的人工智能系统,应用于自动驾驶、智能机器人等领域。", + "keywords": "视觉理解,多模态学习,因果推理,视觉基础模型", + "lab_profile_id": lab_profile.id, + "sort_order": 5 + }, + { + "title": "基于知识图谱的智能问答系统", + "type": "software", + "authors": "李华, 张伟", + "publication_date": date(2023, 6, 1), + "abstract": "本软件是一套基于知识图谱的智能问答系统,支持自然语言问题理解、知识图谱检索、答案生成等功能。系统采用大语言模型结合知识图谱检索增强的技术路线,能够准确回答领域知识问题。", + "keywords": "知识图谱,智能问答,大语言模型,检索增强", + "lab_profile_id": lab_profile.id, + "sort_order": 6 + } + ] + + for ad in achievements_data: + achievement = db.query(ResearchAchievement).filter( + ResearchAchievement.title == ad["title"] + ).first() + if not achievement: + db.add(ResearchAchievement(**ad)) + db.commit() + print("✓ 研究成果已创建") + + members_data = [ + { + "name": "陈博文", + "student_id": "B2023001", + "degree": "博士", + "grade": "2023级", + "major": "计算机科学与技术", + "advisor": "张明教授", + "research_topic": "视觉Transformer高效推理研究", + "email": "chenbw@university.edu.cn", + "status": "active", + "enrollment_date": date(2023, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 1 + }, + { + "name": "刘思远", + "student_id": "B2022002", + "degree": "博士", + "grade": "2022级", + "major": "计算机应用技术", + "advisor": "李华教授", + "research_topic": "大语言模型高效微调", + "email": "liusy@university.edu.cn", + "status": "active", + "enrollment_date": date(2022, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 2 + }, + { + "name": "张伟豪", + "student_id": "M2023001", + "degree": "硕士", + "grade": "2023级", + "major": "计算机科学与技术", + "advisor": "王强副教授", + "research_topic": "多智能体强化学习", + "email": "zhangwh@university.edu.cn", + "status": "active", + "enrollment_date": date(2023, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 3 + }, + { + "name": "李雨晴", + "student_id": "M2023002", + "degree": "硕士", + "grade": "2023级", + "major": "软件工程", + "advisor": "张明教授", + "research_topic": "目标检测与追踪", + "email": "liyq@university.edu.cn", + "status": "active", + "enrollment_date": date(2023, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 4 + }, + { + "name": "王子轩", + "student_id": "M2022001", + "degree": "硕士", + "grade": "2022级", + "major": "计算机科学与技术", + "advisor": "李华教授", + "research_topic": "知识图谱构建", + "email": "wangzx@university.edu.cn", + "status": "active", + "enrollment_date": date(2022, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 5 + }, + { + "name": "陈晓宇", + "student_id": "B2021001", + "degree": "博士", + "grade": "2021级", + "major": "计算机科学与技术", + "advisor": "张明教授", + "research_topic": "医学影像智能分析", + "email": "chenxy@university.edu.cn", + "status": "active", + "enrollment_date": date(2021, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 6 + }, + { + "name": "赵晓彤", + "student_id": "M2022002", + "degree": "硕士", + "grade": "2022级", + "major": "人工智能", + "advisor": "王强副教授", + "research_topic": "强化学习在游戏AI中的应用", + "email": "zhaox@university.edu.cn", + "status": "active", + "enrollment_date": date(2022, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 7 + }, + { + "name": "孙浩然", + "student_id": "B2022003", + "degree": "博士", + "grade": "2022级", + "major": "计算机应用技术", + "advisor": "李华教授", + "research_topic": "文本生成与评估", + "email": "sunhr@university.edu.cn", + "status": "active", + "enrollment_date": date(2022, 9, 1), + "lab_profile_id": lab_profile.id, + "sort_order": 8 + } + ] + + for md in members_data: + member = db.query(LabMember).filter(LabMember.student_id == md["student_id"]).first() + if not member: + db.add(LabMember(**md)) + db.commit() + print("✓ 实验室成员已创建") + + activities_data = [ + { + "title": "2024年春季团建活动", + "description": "实验室全体成员参加了春季团建活动,包括户外拓展训练、团队协作游戏、烧烤聚餐等环节。通过这次活动,大家增进了相互了解,提升了团队凝聚力。", + "activity_date": date(2024, 4, 15), + "location": "北京近郊团建基地", + "participants": "实验室全体教师和学生", + "image_urls": "", + "lab_profile_id": lab_profile.id, + "sort_order": 1 + }, + { + "title": "2023年年终总结会", + "description": "实验室举行2023年年终总结会,各位老师和学生代表汇报了年度工作进展和科研成果。会议还表彰了年度优秀学生,并对2024年的研究工作进行了规划。", + "activity_date": date(2024, 1, 12), + "location": "科研楼会议室", + "participants": "实验室全体成员", + "image_urls": "", + "lab_profile_id": lab_profile.id, + "sort_order": 2 + }, + { + "title": "学术交流研讨会", + "description": "实验室邀请了清华大学、北京大学等知名高校的专家学者进行学术交流,分享最新研究成果。会议期间,实验室研究生也汇报了各自的研究进展。", + "activity_date": date(2023, 11, 20), + "location": "计算机学院学术报告厅", + "participants": "实验室成员及特邀嘉宾", + "image_urls": "", + "lab_profile_id": lab_profile.id, + "sort_order": 3 + }, + { + "title": "2023年新生见面会", + "description": "为欢迎2023级新生加入实验室,举行了新生见面会。各位导师介绍了各自的研究方向,高年级学长分享了科研经验,帮助新生快速适应实验室生活。", + "activity_date": date(2023, 9, 10), + "location": "科研楼A座401", + "participants": "实验室师生", + "image_urls": "", + "lab_profile_id": lab_profile.id, + "sort_order": 4 + } + ] + + for ad in activities_data: + activity = db.query(TeamActivity).filter(TeamActivity.title == ad["title"]).first() + if not activity: + db.add(TeamActivity(**ad)) + db.commit() + print("✓ 团建活动已创建") + + servers_data = [ + { + "name": "GPU服务器01-A100", + "code": "GPU001", + "hostname": "gpu001.ailab.local", + "ip_address": "192.168.1.101", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房A区", + "rack": "A01", + "model": "NVIDIA DGX A100", + "brand": "NVIDIA", + "status": "available", + "is_monitored": True + }, + { + "name": "GPU服务器02-A100", + "code": "GPU002", + "hostname": "gpu002.ailab.local", + "ip_address": "192.168.1.102", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房A区", + "rack": "A01", + "model": "NVIDIA DGX A100", + "brand": "NVIDIA", + "status": "available", + "is_monitored": True + }, + { + "name": "GPU服务器03-V100", + "code": "GPU003", + "hostname": "gpu003.ailab.local", + "ip_address": "192.168.1.103", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房A区", + "rack": "A02", + "model": "NVIDIA DGX V100", + "brand": "NVIDIA", + "status": "available", + "is_monitored": True + }, + { + "name": "GPU服务器04-V100", + "code": "GPU004", + "hostname": "gpu004.ailab.local", + "ip_address": "192.168.1.104", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房A区", + "rack": "A02", + "model": "NVIDIA DGX V100", + "brand": "NVIDIA", + "status": "maintenance", + "is_monitored": True + }, + { + "name": "CPU计算节点01", + "code": "CPU001", + "hostname": "cpu001.ailab.local", + "ip_address": "192.168.1.111", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房B区", + "rack": "B01", + "model": "Dell PowerEdge R750", + "brand": "Dell", + "status": "available", + "is_monitored": True + }, + { + "name": "存储服务器01", + "code": "STOR001", + "hostname": "stor001.ailab.local", + "ip_address": "192.168.1.201", + "ssh_port": 22, + "ssh_user": "admin", + "location": "科研楼机房B区", + "rack": "B02", + "model": "Dell PowerStore 5200", + "brand": "Dell", + "status": "available", + "is_monitored": True + } + ] + + for sd in servers_data: + server = db.query(Server).filter(Server.code == sd["code"]).first() + if not server: + server = Server(**sd) + db.add(server) + db.commit() + db.refresh(server) + + if sd["code"].startswith("GPU"): + resource = ServerResource( + server_id=server.id, + total_cpu_cores=128, + used_cpu_cores=0, + total_cpu_sockets=2, + cpu_model="Intel Xeon Gold 6348", + cpu_frequency="2.6GHz", + total_memory_gb=512.0, + used_memory_gb=0.0, + total_storage_tb=10.0, + used_storage_tb=2.5, + storage_type="NVMe SSD", + total_gpus=8 if "A100" in sd["model"] else 4, + used_gpus=0, + gpu_model="NVIDIA A100 80GB" if "A100" in sd["model"] else "NVIDIA V100 32GB", + gpu_memory_gb=80.0 if "A100" in sd["model"] else 32.0, + network_bandwidth_gbps=100.0, + cpu_usage_percent=15.5, + memory_usage_percent=22.3, + gpu_usage_percent=0.0, + disk_usage_percent=25.0, + uptime_seconds=864000 + ) + elif sd["code"].startswith("CPU"): + resource = ServerResource( + server_id=server.id, + total_cpu_cores=64, + used_cpu_cores=0, + total_cpu_sockets=2, + cpu_model="Intel Xeon Gold 6338", + cpu_frequency="2.0GHz", + total_memory_gb=256.0, + used_memory_gb=0.0, + total_storage_tb=4.0, + used_storage_tb=1.0, + storage_type="SAS SSD", + total_gpus=0, + used_gpus=0, + network_bandwidth_gbps=25.0, + cpu_usage_percent=35.2, + memory_usage_percent=45.8, + gpu_usage_percent=0.0, + disk_usage_percent=25.0, + uptime_seconds=1728000 + ) + else: + resource = ServerResource( + server_id=server.id, + total_cpu_cores=32, + used_cpu_cores=0, + total_cpu_sockets=2, + cpu_model="Intel Xeon Silver 4314", + cpu_frequency="2.4GHz", + total_memory_gb=128.0, + used_memory_gb=0.0, + total_storage_tb=500.0, + used_storage_tb=150.0, + storage_type="HDD RAID", + total_gpus=0, + used_gpus=0, + network_bandwidth_gbps=100.0, + cpu_usage_percent=5.5, + memory_usage_percent=12.3, + gpu_usage_percent=0.0, + disk_usage_percent=30.0, + uptime_seconds=2592000 + ) + db.add(resource) + db.commit() + print("✓ 服务器信息已创建") + + print("\n✅ 示例数据初始化完成!") + print(""" +可使用以下账号登录: +- 管理员:admin / admin123 +- 教师:teacher / teacher123 +- 学生:student / student123 + """) + + except Exception as e: + print(f"❌ 初始化数据失败:{e}") + import traceback + traceback.print_exc() + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + init_sample_data() 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/backend/test.db b/backend/test.db new file mode 100644 index 0000000..1a2893d Binary files /dev/null and b/backend/test.db differ 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..4223dd9 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,415 @@ +import axios from 'axios' +import type { + User, Token, LoginForm, DashboardStats, + Laboratory, Device, Reservation, Experiment, + Attendance, Maintenance, Notification, + LabProfile, Teacher, ResearchDirection, ResearchAchievement, + LabMember, TeamActivity, Server, ServerResource, ComputeTask, + UsageRecord, UsageStats +} 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), + + changePassword: (oldPassword: string, newPassword: string) => + api.put('/auth/me/change-password', { + old_password: oldPassword, + new_password: newPassword + }) +} + +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 const labProfileApi = { + getLabProfiles: (params?: { skip?: number; limit?: number; is_active?: boolean }) => + api.get('/lab-profiles/', { params }), + + getLabProfile: (id: number) => + api.get(`/lab-profiles/${id}`), + + createLabProfile: (data: any) => + api.post('/lab-profiles/', data), + + updateLabProfile: (id: number, data: any) => + api.put(`/lab-profiles/${id}`, data), + + deleteLabProfile: (id: number) => + api.delete(`/lab-profiles/${id}`) +} + +export const teacherApi = { + getTeachers: (params?: { skip?: number; limit?: number; lab_profile_id?: number; is_active?: boolean }) => + api.get('/teachers/', { params }), + + getTeacher: (id: number) => + api.get(`/teachers/${id}`), + + createTeacher: (data: any) => + api.post('/teachers/', data), + + updateTeacher: (id: number, data: any) => + api.put(`/teachers/${id}`, data), + + deleteTeacher: (id: number) => + api.delete(`/teachers/${id}`) +} + +export const researchDirectionApi = { + getResearchDirections: (params?: { skip?: number; limit?: number; lab_profile_id?: number; is_active?: boolean }) => + api.get('/research-directions/', { params }), + + getResearchDirection: (id: number) => + api.get(`/research-directions/${id}`), + + createResearchDirection: (data: any) => + api.post('/research-directions/', data), + + updateResearchDirection: (id: number, data: any) => + api.put(`/research-directions/${id}`, data), + + deleteResearchDirection: (id: number) => + api.delete(`/research-directions/${id}`) +} + +export const researchAchievementApi = { + getAchievements: (params?: { skip?: number; limit?: number; lab_profile_id?: number; type?: string; is_active?: boolean }) => + api.get('/research-achievements/', { params }), + + getAchievement: (id: number) => + api.get(`/research-achievements/${id}`), + + createAchievement: (data: any) => + api.post('/research-achievements/', data), + + updateAchievement: (id: number, data: any) => + api.put(`/research-achievements/${id}`, data), + + deleteAchievement: (id: number) => + api.delete(`/research-achievements/${id}`) +} + +export const labMemberApi = { + getLabMembers: (params?: { skip?: number; limit?: number; lab_profile_id?: number; status?: string; degree?: string }) => + api.get('/lab-members/', { params }), + + getLabMember: (id: number) => + api.get(`/lab-members/${id}`), + + createLabMember: (data: any) => + api.post('/lab-members/', data), + + updateLabMember: (id: number, data: any) => + api.put(`/lab-members/${id}`, data), + + deleteLabMember: (id: number) => + api.delete(`/lab-members/${id}`) +} + +export const teamActivityApi = { + getTeamActivities: (params?: { skip?: number; limit?: number; lab_profile_id?: number; is_active?: boolean }) => + api.get('/team-activities/', { params }), + + getTeamActivity: (id: number) => + api.get(`/team-activities/${id}`), + + createTeamActivity: (data: any) => + api.post('/team-activities/', data), + + updateTeamActivity: (id: number, data: any) => + api.put(`/team-activities/${id}`, data), + + deleteTeamActivity: (id: number) => + api.delete(`/team-activities/${id}`) +} + +export const serverApi = { + getServers: (params?: { skip?: number; limit?: number; status?: string; is_monitored?: boolean }) => + api.get('/servers/', { params }), + + getServer: (id: number) => + api.get(`/servers/${id}`), + + createServer: (data: any) => + api.post('/servers/', data), + + updateServer: (id: number, data: any) => + api.put(`/servers/${id}`, data), + + deleteServer: (id: number) => + api.delete(`/servers/${id}`), + + getServerResources: (id: number) => + api.get(`/servers/${id}/resources`), + + updateServerResources: (id: number, data: any) => + api.put(`/servers/${id}/resources`, data) +} + +export const computeTaskApi = { + getTasks: (params?: { skip?: number; limit?: number; status?: string; server_id?: number; user_id?: number }) => + api.get('/compute-tasks/', { params }), + + getMyTasks: (params?: { skip?: number; limit?: number; status?: string }) => + api.get('/compute-tasks/my', { params }), + + getTask: (id: number) => + api.get(`/compute-tasks/${id}`), + + createTask: (data: any) => + api.post('/compute-tasks/', data), + + submitTask: (data: any) => + api.post('/compute-tasks/submit', data), + + updateTask: (id: number, data: any) => + api.put(`/compute-tasks/${id}`, data), + + cancelTask: (id: number) => + api.post(`/compute-tasks/${id}/cancel`), + + pauseTask: (id: number) => + api.post(`/compute-tasks/${id}/pause`), + + resumeTask: (id: number) => + api.post(`/compute-tasks/${id}/resume`), + + deleteTask: (id: number) => + api.delete(`/compute-tasks/${id}`) +} + +export const usageRecordApi = { + getUsageRecords: (params?: { + skip?: number; + limit?: number; + server_id?: number; + user_id?: number; + task_id?: number; + date_from?: string; + date_to?: string; + }) => + api.get('/usage-records/', { params }), + + getMyUsageRecords: (params?: { + skip?: number; + limit?: number; + server_id?: number; + date_from?: string; + date_to?: string; + }) => + api.get('/usage-records/my', { params }), + + getUsageRecord: (id: number) => + api.get(`/usage-records/${id}`), + + getUsageStats: (params?: { + server_id?: number; + user_id?: number; + date_from?: string; + date_to?: string; + }) => + api.get('/usage-records/stats/summary', { params }) +} + +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..b79d2ba --- /dev/null +++ b/frontend/src/layouts/MainLayout.vue @@ -0,0 +1,385 @@ + + + + + \ 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..3181ae9 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,173 @@ +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: 'lab-intro', + name: 'LabIntro', + component: () => import('../views/lab-intro/LabIntro.vue'), + meta: { title: '实验室介绍' } + }, + { + path: 'lab-profiles', + name: 'LabProfiles', + component: () => import('../views/lab-management/LabProfileList.vue'), + meta: { title: '实验室概况管理', requiresTeacher: true } + }, + { + path: 'teachers-manage', + name: 'TeachersManage', + component: () => import('../views/lab-management/TeacherList.vue'), + meta: { title: '教师信息管理', requiresTeacher: true } + }, + { + path: 'research-directions-manage', + name: 'ResearchDirectionsManage', + component: () => import('../views/lab-management/ResearchDirectionList.vue'), + meta: { title: '研究方向管理', requiresTeacher: true } + }, + { + path: 'research-achievements-manage', + name: 'ResearchAchievementsManage', + component: () => import('../views/lab-management/ResearchAchievementList.vue'), + meta: { title: '研究成果管理', requiresTeacher: true } + }, + { + path: 'lab-members-manage', + name: 'LabMembersManage', + component: () => import('../views/lab-management/LabMemberList.vue'), + meta: { title: '实验室成员管理', requiresTeacher: true } + }, + { + path: 'team-activities-manage', + name: 'TeamActivitiesManage', + component: () => import('../views/lab-management/TeamActivityList.vue'), + meta: { title: '团建活动管理', requiresTeacher: true } + }, + { + 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: 'servers', + name: 'Servers', + component: () => import('../views/server-compute/ServerList.vue'), + meta: { title: '服务器管理' } + }, + { + path: 'tasks', + name: 'Tasks', + component: () => import('../views/server-compute/TaskList.vue'), + meta: { title: '计算任务' } + }, + { + path: 'usage-records', + name: 'UsageRecords', + component: () => import('../views/server-compute/UsageRecords.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..074b791 --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,82 @@ +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 register = async (userData: any) => { + const response = await authApi.register(userData) + 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, + register, + 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..7990f36 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,423 @@ +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 +} + +export interface LabProfile { + id: number + name: string + name_en?: string + code: string + logo_url?: string + location?: string + building?: string + room?: string + established_year?: number + director?: string + director_title?: string + contact_email?: string + contact_phone?: string + introduction?: string + mission?: string + vision?: string + history?: string + facilities?: string + achievements_overview?: string + is_active: boolean + sort_order: number + created_at: string + updated_at?: string + teachers?: Teacher[] + research_directions?: ResearchDirection[] + achievements?: ResearchAchievement[] + members?: LabMember[] + team_activities?: TeamActivity[] +} + +export interface Teacher { + id: number + name: string + name_en?: string + avatar_url?: string + title?: string + position?: string + email?: string + phone?: string + office?: string + research_interests?: string + education?: string + experience?: string + publications?: string + projects?: string + honors?: string + personal_website?: string + lab_profile_id?: number + sort_order: number + is_active: boolean + created_at: string + updated_at?: string +} + +export interface ResearchDirection { + id: number + name: string + name_en?: string + description?: string + icon?: string + key_technologies?: string + applications?: string + lab_profile_id?: number + sort_order: number + is_active: boolean + created_at: string + updated_at?: string +} + +export interface ResearchAchievement { + id: number + title: string + type: string + authors?: string + publication?: string + publication_date?: string + journal?: string + volume?: string + issue?: string + pages?: string + doi?: string + patent_number?: string + project_number?: string + funding_amount?: number + funding_source?: string + abstract?: string + keywords?: string + link_url?: string + google_scholar_url?: string + citations?: number + image_url?: string + lab_profile_id?: number + sort_order: number + is_active: boolean + created_at: string + updated_at?: string +} + +export interface LabMember { + id: number + user_id?: number + name: string + student_id?: string + avatar_url?: string + degree?: string + grade?: string + major?: string + advisor?: string + advisor_id?: number + research_topic?: string + bio?: string + email?: string + phone?: string + enrollment_date?: string + graduation_date?: string + status: string + lab_profile_id?: number + sort_order: number + created_at: string + updated_at?: string +} + +export interface TeamActivity { + id: number + title: string + description?: string + activity_date?: string + location?: string + participants?: string + image_urls?: string + lab_profile_id?: number + sort_order: number + is_active: boolean + created_at: string + updated_at?: string +} + +export interface Server { + id: number + name: string + code: string + hostname?: string + ip_address?: string + ssh_port: number + ssh_user?: string + ssh_password?: string + ssh_key_path?: string + location?: string + rack?: string + model?: string + brand?: string + purchase_date?: string + warranty_expiry?: string + description?: string + status: string + is_monitored: boolean + last_checked_at?: string + created_at: string + updated_at?: string + resources?: ServerResource +} + +export interface ServerResource { + id: number + server_id: number + total_cpu_cores: number + used_cpu_cores: number + total_cpu_sockets: number + cpu_model?: string + cpu_frequency?: string + total_memory_gb: number + used_memory_gb: number + total_storage_tb: number + used_storage_tb: number + storage_type?: string + total_gpus: number + used_gpus: number + gpu_model?: string + gpu_memory_gb: number + network_bandwidth_gbps: number + power_consumption_watts?: number + cpu_usage_percent: number + memory_usage_percent: number + gpu_usage_percent: number + disk_usage_percent: number + uptime_seconds: number + load_average?: string + last_updated?: string + created_at: string + updated_at?: string +} + +export interface ComputeTask { + id: number + name: string + task_id?: string + description?: string + user_id: number + server_id?: number + priority: number + required_cpu_cores: number + required_memory_gb: number + required_gpus: number + required_storage_gpu_memory_gb: number + estimated_duration_hours?: number + workspace_path?: string + script_path?: string + command?: string + environment?: string + container_image?: string + output_path?: string + log_path?: string + status: string + progress: number + queue_position?: number + pid?: number + submit_time?: string + start_time?: string + end_time?: string + actual_duration_seconds?: number + exit_code?: number + error_message?: string + result_summary?: string + allocated_cpu_cores: number + allocated_memory_gb: number + allocated_gpus: number + peak_cpu_usage_percent?: number + peak_memory_usage_gb?: number + gpu_usage?: string + is_public: boolean + parent_task_id?: number + created_at: string + updated_at?: string + user?: User + server?: Server +} + +export interface UsageRecord { + id: number + server_id: number + user_id?: number + task_id?: number + record_date: string + cpu_usage_hours: number + gpu_usage_hours: number + memory_usage_gb_hours: number + storage_usage_tb_hours: number + network_upload_gb: number + network_download_gb: number + peak_cpu_percent?: number + peak_memory_percent?: number + peak_gpu_percent?: number + cost_estimate?: number + created_at: string + updated_at?: string + server?: Server + user?: User + task?: ComputeTask +} + +export interface UsageStats { + total_cpu_hours: number + total_gpu_hours: number + total_memory_gb_hours: number + total_storage_tb_hours: number + total_upload_gb: number + total_download_gb: number + total_records: number + avg_cpu_percent: number + avg_memory_percent: number + avg_gpu_percent: number +} \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..c9b84a7 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,476 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..7970ea3 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..54599ef --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,511 @@ + + + + + \ 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..2ffc241 --- /dev/null +++ b/frontend/src/views/Profile.vue @@ -0,0 +1,353 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/attendance/AttendanceRecord.vue b/frontend/src/views/attendance/AttendanceRecord.vue new file mode 100644 index 0000000..a601846 --- /dev/null +++ b/frontend/src/views/attendance/AttendanceRecord.vue @@ -0,0 +1,479 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/devices/DeviceList.vue b/frontend/src/views/devices/DeviceList.vue new file mode 100644 index 0000000..8e26418 --- /dev/null +++ b/frontend/src/views/devices/DeviceList.vue @@ -0,0 +1,400 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/experiments/ExperimentList.vue b/frontend/src/views/experiments/ExperimentList.vue new file mode 100644 index 0000000..da83886 --- /dev/null +++ b/frontend/src/views/experiments/ExperimentList.vue @@ -0,0 +1,467 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/lab-intro/LabIntro.vue b/frontend/src/views/lab-intro/LabIntro.vue new file mode 100644 index 0000000..3046349 --- /dev/null +++ b/frontend/src/views/lab-intro/LabIntro.vue @@ -0,0 +1,1194 @@ + + + + + diff --git a/frontend/src/views/lab-management/LabMemberList.vue b/frontend/src/views/lab-management/LabMemberList.vue new file mode 100644 index 0000000..04e587d --- /dev/null +++ b/frontend/src/views/lab-management/LabMemberList.vue @@ -0,0 +1,714 @@ + + + + + diff --git a/frontend/src/views/lab-management/LabProfileList.vue b/frontend/src/views/lab-management/LabProfileList.vue new file mode 100644 index 0000000..efc77bb --- /dev/null +++ b/frontend/src/views/lab-management/LabProfileList.vue @@ -0,0 +1,511 @@ + + + + + diff --git a/frontend/src/views/lab-management/ResearchAchievementList.vue b/frontend/src/views/lab-management/ResearchAchievementList.vue new file mode 100644 index 0000000..c146cfa --- /dev/null +++ b/frontend/src/views/lab-management/ResearchAchievementList.vue @@ -0,0 +1,572 @@ + + + + + diff --git a/frontend/src/views/lab-management/ResearchDirectionList.vue b/frontend/src/views/lab-management/ResearchDirectionList.vue new file mode 100644 index 0000000..01152f9 --- /dev/null +++ b/frontend/src/views/lab-management/ResearchDirectionList.vue @@ -0,0 +1,341 @@ + + + + + diff --git a/frontend/src/views/lab-management/TeacherList.vue b/frontend/src/views/lab-management/TeacherList.vue new file mode 100644 index 0000000..964409e --- /dev/null +++ b/frontend/src/views/lab-management/TeacherList.vue @@ -0,0 +1,659 @@ + + + + + diff --git a/frontend/src/views/lab-management/TeamActivityList.vue b/frontend/src/views/lab-management/TeamActivityList.vue new file mode 100644 index 0000000..abf9fe6 --- /dev/null +++ b/frontend/src/views/lab-management/TeamActivityList.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/frontend/src/views/labs/LabList.vue b/frontend/src/views/labs/LabList.vue new file mode 100644 index 0000000..04cf786 --- /dev/null +++ b/frontend/src/views/labs/LabList.vue @@ -0,0 +1,357 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/maintenance/MaintenanceList.vue b/frontend/src/views/maintenance/MaintenanceList.vue new file mode 100644 index 0000000..a792aee --- /dev/null +++ b/frontend/src/views/maintenance/MaintenanceList.vue @@ -0,0 +1,412 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/notifications/NotificationList.vue b/frontend/src/views/notifications/NotificationList.vue new file mode 100644 index 0000000..4c895e3 --- /dev/null +++ b/frontend/src/views/notifications/NotificationList.vue @@ -0,0 +1,294 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/reservations/ReservationList.vue b/frontend/src/views/reservations/ReservationList.vue new file mode 100644 index 0000000..d72e62e --- /dev/null +++ b/frontend/src/views/reservations/ReservationList.vue @@ -0,0 +1,410 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/server-compute/ServerList.vue b/frontend/src/views/server-compute/ServerList.vue new file mode 100644 index 0000000..d3740ce --- /dev/null +++ b/frontend/src/views/server-compute/ServerList.vue @@ -0,0 +1,833 @@ + + + + + diff --git a/frontend/src/views/server-compute/TaskList.vue b/frontend/src/views/server-compute/TaskList.vue new file mode 100644 index 0000000..9c4358d --- /dev/null +++ b/frontend/src/views/server-compute/TaskList.vue @@ -0,0 +1,701 @@ + + + + + diff --git a/frontend/src/views/server-compute/UsageRecords.vue b/frontend/src/views/server-compute/UsageRecords.vue new file mode 100644 index 0000000..00c374d --- /dev/null +++ b/frontend/src/views/server-compute/UsageRecords.vue @@ -0,0 +1,594 @@ + + + + + diff --git a/frontend/src/views/users/UserList.vue b/frontend/src/views/users/UserList.vue new file mode 100644 index 0000000..7061665 --- /dev/null +++ b/frontend/src/views/users/UserList.vue @@ -0,0 +1,462 @@ + + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..9e1abed --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..099658c --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e39bec9 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + } +}) \ No newline at end of file