diff --git a/backend/__pycache__/auth.cpython-310.pyc b/backend/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000..472fea6 Binary files /dev/null and b/backend/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/__pycache__/calculations.cpython-310.pyc b/backend/__pycache__/calculations.cpython-310.pyc new file mode 100644 index 0000000..fe2fe9f Binary files /dev/null and b/backend/__pycache__/calculations.cpython-310.pyc differ diff --git a/backend/__pycache__/config.cpython-310.pyc b/backend/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..ec6d96a Binary files /dev/null and b/backend/__pycache__/config.cpython-310.pyc differ diff --git a/backend/__pycache__/database.cpython-310.pyc b/backend/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000..aec53ff Binary files /dev/null and b/backend/__pycache__/database.cpython-310.pyc differ diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..48af3a8 Binary files /dev/null and b/backend/__pycache__/main.cpython-310.pyc differ diff --git a/backend/__pycache__/models.cpython-310.pyc b/backend/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..f422413 Binary files /dev/null and b/backend/__pycache__/models.cpython-310.pyc differ diff --git a/backend/__pycache__/schemas.cpython-310.pyc b/backend/__pycache__/schemas.cpython-310.pyc new file mode 100644 index 0000000..1f288d7 Binary files /dev/null and b/backend/__pycache__/schemas.cpython-310.pyc differ diff --git a/backend/airlabs.db b/backend/airlabs.db new file mode 100644 index 0000000..91408df Binary files /dev/null and b/backend/airlabs.db differ diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..e6a88cb --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,62 @@ +"""JWT 认证 + 权限控制""" +from datetime import datetime, timedelta +from typing import Optional +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.orm import Session +from database import get_db +from models import User, UserRole +from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain: str, hashed: str) -> bool: + return pwd_context.verify(plain, hashed) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: + """从 JWT token 解析当前用户""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="登录已过期,请重新登录", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: int = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = db.query(User).filter(User.id == user_id).first() + if user is None or not user.is_active: + raise credentials_exception + return user + + +def require_role(*roles: UserRole): + """权限装饰器:要求当前用户具有指定角色之一""" + def role_checker(current_user: User = Depends(get_current_user)): + if current_user.role not in [r.value for r in roles] and current_user.role not in roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="权限不足" + ) + return current_user + return role_checker diff --git a/backend/calculations.py b/backend/calculations.py new file mode 100644 index 0000000..c678849 --- /dev/null +++ b/backend/calculations.py @@ -0,0 +1,271 @@ +""" +计算引擎 —— 所有成本分摊、损耗、效率计算逻辑集中在此模块 +修改计算规则只需改此文件。 +""" +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func, and_ +from collections import defaultdict +from datetime import date, timedelta +from models import ( + User, Project, Submission, AIToolCost, AIToolCostAllocation, + OutsourceCost, CostOverride, WorkType, CostAllocationType +) +from config import WORKING_DAYS_PER_MONTH + + +# ──────────────────────────── 人力成本分摊 ──────────────────────────── + +def calc_labor_cost_for_project(project_id: int, db: Session) -> float: + """ + 计算某项目的累计人力成本 + 规则: + - 有秒数的提交 → 按各项目产出秒数比例分摊日成本 + - 无秒数的提交 → 按各项目提交条数比例分摊日成本 + - 管理员手动调整优先 + """ + # 找出所有给此项目提交过的人 + submitters = db.query(Submission.user_id).filter( + Submission.project_id == project_id + ).distinct().all() + submitter_ids = [s[0] for s in submitters] + + total_labor = 0.0 + + for uid in submitter_ids: + user = db.query(User).filter(User.id == uid).first() + if not user: + continue + daily_cost = user.daily_cost + + # 找这个人在此项目的所有提交日期 + dates = db.query(Submission.submit_date).filter( + Submission.user_id == uid, + Submission.project_id == project_id, + ).distinct().all() + + for (d,) in dates: + # 检查是否有手动调整 + override = db.query(CostOverride).filter( + CostOverride.user_id == uid, + CostOverride.date == d, + CostOverride.project_id == project_id, + ).first() + if override: + total_labor += override.override_amount + continue + + # 这个人这天所有项目的提交 + day_subs = db.query(Submission).filter( + Submission.user_id == uid, + Submission.submit_date == d, + ).all() + + # 计算这天各项目的秒数和条数 + project_seconds = defaultdict(float) + project_counts = defaultdict(int) + total_day_seconds = 0.0 + total_day_count = 0 + + for s in day_subs: + project_seconds[s.project_id] += s.total_seconds + project_counts[s.project_id] += 1 + total_day_seconds += s.total_seconds + total_day_count += 1 + + # 分摊 + if total_day_seconds > 0: + # 有秒数 → 按秒数比例 + ratio = project_seconds.get(project_id, 0) / total_day_seconds + elif total_day_count > 0: + # 无秒数 → 按条数比例 + ratio = project_counts.get(project_id, 0) / total_day_count + else: + ratio = 0 + + total_labor += daily_cost * ratio + + return round(total_labor, 2) + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +def calc_ai_tool_cost_for_project(project_id: int, db: Session) -> float: + """计算某项目的 AI 工具成本""" + total = 0.0 + + # 1. 直接指定项目的 + direct = db.query(sa_func.sum(AIToolCost.amount)).filter( + AIToolCost.allocation_type == CostAllocationType.PROJECT, + AIToolCost.project_id == project_id, + ).scalar() or 0 + total += direct + + # 2. 手动分摊的 + manual = db.query(AIToolCostAllocation).filter( + AIToolCostAllocation.project_id == project_id, + ).all() + for alloc in manual: + cost = db.query(AIToolCost).filter(AIToolCost.id == alloc.ai_tool_cost_id).first() + if cost: + total += cost.amount * alloc.percentage / 100 + + # 3. 内容组整体(按产出秒数比例分摊) + team_costs = db.query(AIToolCost).filter( + AIToolCost.allocation_type == CostAllocationType.TEAM, + ).all() + if team_costs: + # 所有项目的总秒数 + all_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.total_seconds > 0 + ).scalar() or 0 + # 此项目的秒数 + proj_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).scalar() or 0 + + if all_secs > 0: + ratio = proj_secs / all_secs + for c in team_costs: + total += c.amount * ratio + + return round(total, 2) + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +def calc_outsource_cost_for_project(project_id: int, db: Session) -> float: + """计算某项目的外包成本""" + total = db.query(sa_func.sum(OutsourceCost.amount)).filter( + OutsourceCost.project_id == project_id, + ).scalar() or 0 + return round(total, 2) + + +# ──────────────────────────── 损耗计算 ──────────────────────────── + +def calc_waste_for_project(project_id: int, db: Session) -> dict: + """ + 计算项目损耗 + 返回: {test_waste, overproduction_waste, total_waste, waste_rate, target_seconds} + """ + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return {} + + target = project.target_total_seconds + + # 测试损耗:工作类型为"测试"的全部秒数 + test_waste = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.work_type == WorkType.TEST, + ).scalar() or 0 + + # 全部有秒数的提交总量 + total_submitted = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).scalar() or 0 + + # 超产损耗 + overproduction_waste = max(0, total_submitted - target) + + total_waste = test_waste + overproduction_waste + waste_rate = round(total_waste / target * 100, 1) if target > 0 else 0 + + return { + "target_seconds": target, + "total_submitted_seconds": round(total_submitted, 1), + "test_waste_seconds": round(test_waste, 1), + "overproduction_waste_seconds": round(overproduction_waste, 1), + "total_waste_seconds": round(total_waste, 1), + "waste_rate": waste_rate, + } + + +# ──────────────────────────── 团队效率 ──────────────────────────── + +def calc_team_efficiency(project_id: int, db: Session) -> list: + """ + 人均基准对比法: + - 人均基准 = 目标秒数 ÷ 参与制作人数 + - 每人超出比例 = (个人提交 - 人均基准) / 人均基准 + """ + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return [] + + target = project.target_total_seconds + + # 获取每个人的提交总秒数(仅有秒数的提交) + per_user = db.query( + Submission.user_id, + sa_func.sum(Submission.total_seconds).label("total_secs"), + sa_func.count(Submission.id).label("count"), + ).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).group_by(Submission.user_id).all() + + if not per_user: + return [] + + num_people = len(per_user) + baseline = target / num_people if num_people > 0 else 0 + + result = [] + for user_id, total_secs, count in per_user: + user = db.query(User).filter(User.id == user_id).first() + excess = total_secs - baseline + excess_rate = round(excess / baseline * 100, 1) if baseline > 0 else 0 + result.append({ + "user_id": user_id, + "user_name": user.name if user else "未知", + "total_seconds": round(total_secs, 1), + "submission_count": count, + "baseline": round(baseline, 1), + "excess_seconds": round(excess, 1), + "excess_rate": excess_rate, + }) + + result.sort(key=lambda x: x["total_seconds"], reverse=True) + return result + + +# ──────────────────────────── 项目完整结算 ──────────────────────────── + +def calc_project_settlement(project_id: int, db: Session) -> dict: + """生成项目结算报告""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + return {} + + labor = calc_labor_cost_for_project(project_id, db) + ai_tool = calc_ai_tool_cost_for_project(project_id, db) + outsource = calc_outsource_cost_for_project(project_id, db) + total_cost = labor + ai_tool + outsource + waste = calc_waste_for_project(project_id, db) + efficiency = calc_team_efficiency(project_id, db) + + result = { + "project_id": project.id, + "project_name": project.name, + "project_type": project.project_type.value if hasattr(project.project_type, 'value') else project.project_type, + "labor_cost": labor, + "ai_tool_cost": ai_tool, + "outsource_cost": outsource, + "total_cost": round(total_cost, 2), + **waste, + "team_efficiency": efficiency, + } + + # 客户正式项目计算盈亏 + pt = project.project_type.value if hasattr(project.project_type, 'value') else project.project_type + if pt == "客户正式项目" and project.contract_amount: + result["contract_amount"] = project.contract_amount + result["profit_loss"] = round(project.contract_amount - total_cost, 2) + else: + result["contract_amount"] = None + result["profit_loss"] = None + + return result diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..e25ee8d --- /dev/null +++ b/backend/config.py @@ -0,0 +1,13 @@ +"""应用配置""" +import os + +# 数据库 +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./airlabs.db") + +# JWT 认证 +SECRET_KEY = os.getenv("SECRET_KEY", "airlabs-project-secret-key-change-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 小时 + +# 成本计算 +WORKING_DAYS_PER_MONTH = 22 diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..3c26832 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,17 @@ +"""数据库初始化""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +from config import DATABASE_URL + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + """获取数据库会话(FastAPI 依赖注入用)""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..ffccbdc --- /dev/null +++ b/backend/main.py @@ -0,0 +1,72 @@ +"""AirLabs Project —— 主入口""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from database import engine, Base +from models import User, UserRole, PhaseGroup +from auth import hash_password +import os + +# 创建所有表 +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="AirLabs Project", version="1.0.0") + +# CORS(开发阶段允许所有来源) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +from routers.auth import router as auth_router +from routers.users import router as users_router +from routers.projects import router as projects_router +from routers.submissions import router as submissions_router +from routers.costs import router as costs_router +from routers.dashboard import router as dashboard_router + +app.include_router(auth_router) +app.include_router(users_router) +app.include_router(projects_router) +app.include_router(submissions_router) +app.include_router(costs_router) +app.include_router(dashboard_router) + +# 前端静态文件 +frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist") +if os.path.exists(frontend_dir): + app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="assets") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + file_path = os.path.join(frontend_dir, full_path) + if os.path.isfile(file_path): + return FileResponse(file_path) + return FileResponse(os.path.join(frontend_dir, "index.html")) + + +@app.on_event("startup") +def init_default_owner(): + """首次启动时创建默认 Owner 账号""" + from database import SessionLocal + db = SessionLocal() + try: + if not db.query(User).filter(User.role == UserRole.OWNER).first(): + owner = User( + username="admin", + password_hash=hash_password("admin123"), + name="管理员", + phase_group=PhaseGroup.PRODUCTION, + role=UserRole.OWNER, + monthly_salary=0, + ) + db.add(owner) + db.commit() + print("[OK] default admin created: admin / admin123") + finally: + db.close() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..ab32cb1 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,238 @@ +"""数据库模型 —— 所有表定义""" +from sqlalchemy import ( + Column, Integer, String, Float, Date, DateTime, Text, + ForeignKey, Enum as SAEnum, JSON +) +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +# ──────────────────────────── 枚举定义 ──────────────────────────── + +class ProjectType(str, enum.Enum): + CLIENT_FORMAL = "客户正式项目" + CLIENT_TEST = "客户测试项目" + INTERNAL_ORIGINAL = "内部原创项目" + INTERNAL_TEST = "内部测试项目" + + +class ProjectStatus(str, enum.Enum): + IN_PROGRESS = "制作中" + COMPLETED = "已完成" + + +class PhaseGroup(str, enum.Enum): + PRE = "前期" + PRODUCTION = "制作" + POST = "后期" + + +class UserRole(str, enum.Enum): + MEMBER = "成员" + LEADER = "组长" + SUPERVISOR = "主管" + OWNER = "Owner" + + +class WorkType(str, enum.Enum): + PRODUCTION = "制作" + TEST = "测试" + PLAN = "方案" + + +class ContentType(str, enum.Enum): + ANIMATION = "内容制作" + DESIGN = "设定策划" + EDITING = "剪辑后期" + OTHER = "其他" + + +class SubmitTo(str, enum.Enum): + LEADER = "组长" + PRODUCER = "制片" + INTERNAL = "内部" + EXTERNAL = "外部" + + +class SubscriptionPeriod(str, enum.Enum): + MONTHLY = "月" + YEARLY = "年" + + +class CostAllocationType(str, enum.Enum): + PROJECT = "指定项目" + TEAM = "内容组整体" + MANUAL = "手动分摊" + + +class OutsourceType(str, enum.Enum): + ANIMATION = "动画" + EDITING = "剪辑" + FULL_EPISODE = "整集" + + +# ──────────────────────────── 用户 ──────────────────────────── + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, nullable=False, index=True) + password_hash = Column(String(255), nullable=False) + name = Column(String(50), nullable=False) + phase_group = Column(SAEnum(PhaseGroup), nullable=False) + role = Column(SAEnum(UserRole), nullable=False, default=UserRole.MEMBER) + monthly_salary = Column(Float, nullable=False, default=0) + is_active = Column(Integer, nullable=False, default=1) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + submissions = relationship("Submission", back_populates="user") + led_projects = relationship("Project", back_populates="leader") + + @property + def daily_cost(self): + """日成本 = 月薪 ÷ 22""" + from config import WORKING_DAYS_PER_MONTH + return round(self.monthly_salary / WORKING_DAYS_PER_MONTH, 2) if self.monthly_salary else 0 + + +# ──────────────────────────── 项目 ──────────────────────────── + +class Project(Base): + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + project_type = Column(SAEnum(ProjectType), nullable=False) + status = Column(SAEnum(ProjectStatus), nullable=False, default=ProjectStatus.IN_PROGRESS) + leader_id = Column(Integer, ForeignKey("users.id"), nullable=True) + current_phase = Column(SAEnum(PhaseGroup), nullable=False, default=PhaseGroup.PRE) + episode_duration_minutes = Column(Float, nullable=False) + episode_count = Column(Integer, nullable=False) + estimated_completion_date = Column(Date, nullable=True) + actual_completion_date = Column(Date, nullable=True) + contract_amount = Column(Float, nullable=True) # 仅客户正式项目 + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + leader = relationship("User", back_populates="led_projects") + submissions = relationship("Submission", back_populates="project") + outsource_costs = relationship("OutsourceCost", back_populates="project") + ai_tool_allocations = relationship("AIToolCostAllocation", back_populates="project") + + @property + def target_total_seconds(self): + """目标总秒数 = 单集时长(分) × 60 × 集数""" + return int(self.episode_duration_minutes * 60 * self.episode_count) + + +# ──────────────────────────── 内容提交 ──────────────────────────── + +class Submission(Base): + __tablename__ = "submissions" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + project_phase = Column(SAEnum(PhaseGroup), nullable=False) + work_type = Column(SAEnum(WorkType), nullable=False) + content_type = Column(SAEnum(ContentType), nullable=False) + duration_minutes = Column(Float, nullable=True, default=0) + duration_seconds = Column(Float, nullable=True, default=0) + total_seconds = Column(Float, nullable=False, default=0) # 系统自动计算 + hours_spent = Column(Float, nullable=True) # 可选:投入时长(小时) + submit_to = Column(SAEnum(SubmitTo), nullable=False) + description = Column(Text, nullable=True) + submit_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + user = relationship("User", back_populates="submissions") + project = relationship("Project", back_populates="submissions") + history = relationship("SubmissionHistory", back_populates="submission") + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +class AIToolCost(Base): + __tablename__ = "ai_tool_costs" + + id = Column(Integer, primary_key=True, index=True) + tool_name = Column(String(100), nullable=False) + subscription_period = Column(SAEnum(SubscriptionPeriod), nullable=False) + amount = Column(Float, nullable=False) + allocation_type = Column(SAEnum(CostAllocationType), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=True) # 指定项目时 + recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False) + record_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + # 关系 + allocations = relationship("AIToolCostAllocation", back_populates="ai_tool_cost") + + +class AIToolCostAllocation(Base): + """AI 工具成本手动分摊明细""" + __tablename__ = "ai_tool_cost_allocations" + + id = Column(Integer, primary_key=True, index=True) + ai_tool_cost_id = Column(Integer, ForeignKey("ai_tool_costs.id"), nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + percentage = Column(Float, nullable=False) # 0-100 + + ai_tool_cost = relationship("AIToolCost", back_populates="allocations") + project = relationship("Project", back_populates="ai_tool_allocations") + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +class OutsourceCost(Base): + __tablename__ = "outsource_costs" + + id = Column(Integer, primary_key=True, index=True) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + outsource_type = Column(SAEnum(OutsourceType), nullable=False) + episode_start = Column(Integer, nullable=True) + episode_end = Column(Integer, nullable=True) + amount = Column(Float, nullable=False) + recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False) + record_date = Column(Date, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + project = relationship("Project", back_populates="outsource_costs") + + +# ──────────────────────────── 人力成本手动调整 ──────────────────────────── + +class CostOverride(Base): + """管理员手动修改某人某天的成本分摊""" + __tablename__ = "cost_overrides" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + date = Column(Date, nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) + override_amount = Column(Float, nullable=False) + adjusted_by = Column(Integer, ForeignKey("users.id"), nullable=False) + reason = Column(Text, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + + +# ──────────────────────────── 提交历史版本 ──────────────────────────── + +class SubmissionHistory(Base): + """内容提交的修改历史""" + __tablename__ = "submission_history" + + id = Column(Integer, primary_key=True, index=True) + submission_id = Column(Integer, ForeignKey("submissions.id"), nullable=False) + changed_by = Column(Integer, ForeignKey("users.id"), nullable=False) + change_reason = Column(Text, nullable=False) + old_data = Column(JSON, nullable=False) + new_data = Column(JSON, nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + submission = relationship("Submission", back_populates="history") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..38c85ae --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +sqlalchemy +pydantic +python-jose[cryptography] +passlib[bcrypt] +python-multipart diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/routers/__pycache__/__init__.cpython-310.pyc b/backend/routers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..c5cc7b4 Binary files /dev/null and b/backend/routers/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/auth.cpython-310.pyc b/backend/routers/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000..851f532 Binary files /dev/null and b/backend/routers/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/costs.cpython-310.pyc b/backend/routers/__pycache__/costs.cpython-310.pyc new file mode 100644 index 0000000..f3ee76a Binary files /dev/null and b/backend/routers/__pycache__/costs.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/dashboard.cpython-310.pyc b/backend/routers/__pycache__/dashboard.cpython-310.pyc new file mode 100644 index 0000000..d10805c Binary files /dev/null and b/backend/routers/__pycache__/dashboard.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/projects.cpython-310.pyc b/backend/routers/__pycache__/projects.cpython-310.pyc new file mode 100644 index 0000000..330608e Binary files /dev/null and b/backend/routers/__pycache__/projects.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/submissions.cpython-310.pyc b/backend/routers/__pycache__/submissions.cpython-310.pyc new file mode 100644 index 0000000..530a0d0 Binary files /dev/null and b/backend/routers/__pycache__/submissions.cpython-310.pyc differ diff --git a/backend/routers/__pycache__/users.cpython-310.pyc b/backend/routers/__pycache__/users.cpython-310.pyc new file mode 100644 index 0000000..7234d28 Binary files /dev/null and b/backend/routers/__pycache__/users.cpython-310.pyc differ diff --git a/backend/routers/auth.py b/backend/routers/auth.py new file mode 100644 index 0000000..8ee2375 --- /dev/null +++ b/backend/routers/auth.py @@ -0,0 +1,35 @@ +"""认证路由""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from database import get_db +from models import User +from schemas import LoginRequest, Token, UserOut +from auth import verify_password, create_access_token, get_current_user + +router = APIRouter(prefix="/api/auth", tags=["认证"]) + + +@router.post("/login", response_model=Token) +def login(req: LoginRequest, db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == req.username).first() + if not user or not verify_password(req.password, user.password_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误") + if not user.is_active: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已停用") + token = create_access_token(data={"sub": user.id}) + return {"access_token": token, "token_type": "bearer"} + + +@router.get("/me", response_model=UserOut) +def get_me(current_user: User = Depends(get_current_user)): + return UserOut( + id=current_user.id, + username=current_user.username, + name=current_user.name, + phase_group=current_user.phase_group.value if hasattr(current_user.phase_group, 'value') else current_user.phase_group, + role=current_user.role.value if hasattr(current_user.role, 'value') else current_user.role, + monthly_salary=current_user.monthly_salary, + daily_cost=current_user.daily_cost, + is_active=current_user.is_active, + created_at=current_user.created_at, + ) diff --git a/backend/routers/costs.py b/backend/routers/costs.py new file mode 100644 index 0000000..86e2326 --- /dev/null +++ b/backend/routers/costs.py @@ -0,0 +1,205 @@ +"""成本管理路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +from datetime import date +from database import get_db +from models import ( + User, UserRole, AIToolCost, AIToolCostAllocation, OutsourceCost, + CostOverride, SubscriptionPeriod, CostAllocationType, OutsourceType +) +from schemas import ( + AIToolCostCreate, AIToolCostOut, OutsourceCostCreate, OutsourceCostOut, + CostOverrideCreate +) +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/costs", tags=["成本管理"]) + + +# ──────────────────── AI 工具成本 ──────────────────── + +@router.get("/ai-tools", response_model=List[AIToolCostOut]) +def list_ai_tool_costs( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + costs = db.query(AIToolCost).order_by(AIToolCost.record_date.desc()).all() + return [ + AIToolCostOut( + id=c.id, tool_name=c.tool_name, + subscription_period=c.subscription_period.value if hasattr(c.subscription_period, 'value') else c.subscription_period, + amount=c.amount, + allocation_type=c.allocation_type.value if hasattr(c.allocation_type, 'value') else c.allocation_type, + project_id=c.project_id, + recorded_by=c.recorded_by, + record_date=c.record_date, + created_at=c.created_at, + ) + for c in costs + ] + + +@router.post("/ai-tools", response_model=AIToolCostOut) +def create_ai_tool_cost( + req: AIToolCostCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + cost = AIToolCost( + tool_name=req.tool_name, + subscription_period=SubscriptionPeriod(req.subscription_period), + amount=req.amount, + allocation_type=CostAllocationType(req.allocation_type), + project_id=req.project_id, + recorded_by=current_user.id, + record_date=req.record_date, + ) + db.add(cost) + db.flush() + + # 处理手动分摊 + if req.allocation_type == "手动分摊" and req.allocations: + for alloc in req.allocations: + db.add(AIToolCostAllocation( + ai_tool_cost_id=cost.id, + project_id=alloc["project_id"], + percentage=alloc["percentage"], + )) + db.commit() + db.refresh(cost) + return AIToolCostOut( + id=cost.id, tool_name=cost.tool_name, + subscription_period=cost.subscription_period.value, + amount=cost.amount, + allocation_type=cost.allocation_type.value, + project_id=cost.project_id, + recorded_by=cost.recorded_by, + record_date=cost.record_date, + created_at=cost.created_at, + ) + + +@router.delete("/ai-tools/{cost_id}") +def delete_ai_tool_cost( + cost_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + cost = db.query(AIToolCost).filter(AIToolCost.id == cost_id).first() + if not cost: + raise HTTPException(status_code=404, detail="记录不存在") + db.query(AIToolCostAllocation).filter(AIToolCostAllocation.ai_tool_cost_id == cost_id).delete() + db.delete(cost) + db.commit() + return {"message": "已删除"} + + +# ──────────────────── 外包成本 ──────────────────── + +@router.get("/outsource", response_model=List[OutsourceCostOut]) +def list_outsource_costs( + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + q = db.query(OutsourceCost) + if project_id: + q = q.filter(OutsourceCost.project_id == project_id) + costs = q.order_by(OutsourceCost.record_date.desc()).all() + return [ + OutsourceCostOut( + id=c.id, project_id=c.project_id, + outsource_type=c.outsource_type.value if hasattr(c.outsource_type, 'value') else c.outsource_type, + episode_start=c.episode_start, episode_end=c.episode_end, + amount=c.amount, recorded_by=c.recorded_by, + record_date=c.record_date, created_at=c.created_at, + ) + for c in costs + ] + + +@router.post("/outsource", response_model=OutsourceCostOut) +def create_outsource_cost( + req: OutsourceCostCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + cost = OutsourceCost( + project_id=req.project_id, + outsource_type=OutsourceType(req.outsource_type), + episode_start=req.episode_start, + episode_end=req.episode_end, + amount=req.amount, + recorded_by=current_user.id, + record_date=req.record_date, + ) + db.add(cost) + db.commit() + db.refresh(cost) + return OutsourceCostOut( + id=cost.id, project_id=cost.project_id, + outsource_type=cost.outsource_type.value, + episode_start=cost.episode_start, episode_end=cost.episode_end, + amount=cost.amount, recorded_by=cost.recorded_by, + record_date=cost.record_date, created_at=cost.created_at, + ) + + +@router.delete("/outsource/{cost_id}") +def delete_outsource_cost( + cost_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + cost = db.query(OutsourceCost).filter(OutsourceCost.id == cost_id).first() + if not cost: + raise HTTPException(status_code=404, detail="记录不存在") + db.delete(cost) + db.commit() + return {"message": "已删除"} + + +# ──────────────────── 人力成本手动调整 ──────────────────── + +@router.post("/overrides") +def create_cost_override( + req: CostOverrideCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + override = CostOverride( + user_id=req.user_id, + date=req.date, + project_id=req.project_id, + override_amount=req.override_amount, + adjusted_by=current_user.id, + reason=req.reason, + ) + db.add(override) + db.commit() + return {"message": "已保存成本调整"} + + +@router.get("/overrides") +def list_cost_overrides( + user_id: Optional[int] = Query(None), + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + q = db.query(CostOverride) + if user_id: + q = q.filter(CostOverride.user_id == user_id) + if project_id: + q = q.filter(CostOverride.project_id == project_id) + records = q.order_by(CostOverride.date.desc()).all() + return [ + { + "id": r.id, "user_id": r.user_id, "date": r.date, + "project_id": r.project_id, "override_amount": r.override_amount, + "adjusted_by": r.adjusted_by, "reason": r.reason, + "created_at": r.created_at, + } + for r in records + ] diff --git a/backend/routers/dashboard.py b/backend/routers/dashboard.py new file mode 100644 index 0000000..7e2595d --- /dev/null +++ b/backend/routers/dashboard.py @@ -0,0 +1,152 @@ +"""仪表盘 + 结算路由""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func +from datetime import date, timedelta +from database import get_db +from models import ( + User, UserRole, Project, Submission, AIToolCost, + ProjectStatus, ProjectType, WorkType +) +from auth import get_current_user, require_role +from calculations import ( + calc_project_settlement, calc_waste_for_project, + calc_labor_cost_for_project, calc_ai_tool_cost_for_project, + calc_outsource_cost_for_project, calc_team_efficiency +) + +router = APIRouter(prefix="/api", tags=["仪表盘与结算"]) + + +@router.get("/dashboard") +def get_dashboard( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """全局仪表盘数据""" + # 项目概览 + active = db.query(Project).filter(Project.status == ProjectStatus.IN_PROGRESS).all() + completed = db.query(Project).filter(Project.status == ProjectStatus.COMPLETED).all() + + # 当月日期范围 + today = date.today() + month_start = today.replace(day=1) + + # 本月人力成本(简化:统计本月所有有提交的人的日成本) + monthly_submitters = db.query(Submission.user_id, Submission.submit_date).filter( + Submission.submit_date >= month_start, + Submission.submit_date <= today, + ).distinct().all() + + monthly_labor = 0.0 + processed_user_dates = set() + for uid, d in monthly_submitters: + key = (uid, d) + if key not in processed_user_dates: + processed_user_dates.add(key) + user = db.query(User).filter(User.id == uid).first() + if user: + monthly_labor += user.daily_cost + + # 本月 AI 工具成本 + monthly_ai = db.query(sa_func.sum(AIToolCost.amount)).filter( + AIToolCost.record_date >= month_start, + AIToolCost.record_date <= today, + ).scalar() or 0 + + # 本月总产出秒数 + monthly_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.submit_date >= month_start, + Submission.submit_date <= today, + Submission.total_seconds > 0, + ).scalar() or 0 + + # 活跃人数 + active_users = db.query(Submission.user_id).filter( + Submission.submit_date >= month_start, + ).distinct().count() + working_days = max(1, (today - month_start).days + 1) + avg_daily = round(monthly_secs / max(1, active_users) / working_days, 1) + + # 各项目摘要 + project_summaries = [] + for p in active: + waste = calc_waste_for_project(p.id, db) + total_secs = waste.get("total_submitted_seconds", 0) + target = p.target_total_seconds + progress = round(total_secs / target * 100, 1) if target > 0 else 0 + is_overdue = ( + p.estimated_completion_date and today > p.estimated_completion_date + ) + project_summaries.append({ + "id": p.id, + "name": p.name, + "project_type": p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, + "progress_percent": progress, + "target_seconds": target, + "submitted_seconds": total_secs, + "waste_rate": waste.get("waste_rate", 0), + "is_overdue": bool(is_overdue), + "estimated_completion_date": str(p.estimated_completion_date) if p.estimated_completion_date else None, + }) + + # 损耗排行 + waste_ranking = [] + for p in active + completed: + w = calc_waste_for_project(p.id, db) + if w.get("total_waste_seconds", 0) > 0: + waste_ranking.append({ + "project_id": p.id, + "project_name": p.name, + "waste_seconds": w["total_waste_seconds"], + "waste_rate": w["waste_rate"], + }) + waste_ranking.sort(key=lambda x: x["waste_rate"], reverse=True) + + # 已结算项目 + settled = [] + for p in completed: + settlement = calc_project_settlement(p.id, db) + settled.append({ + "project_id": p.id, + "project_name": p.name, + "project_type": settlement.get("project_type", ""), + "total_cost": settlement.get("total_cost", 0), + "contract_amount": settlement.get("contract_amount"), + "profit_loss": settlement.get("profit_loss"), + }) + + return { + "active_projects": len(active), + "completed_projects": len(completed), + "monthly_labor_cost": round(monthly_labor, 2), + "monthly_ai_tool_cost": round(monthly_ai, 2), + "monthly_total_seconds": round(monthly_secs, 1), + "avg_daily_seconds_per_person": avg_daily, + "projects": project_summaries, + "waste_ranking": waste_ranking, + "settled_projects": settled, + } + + +@router.get("/projects/{project_id}/settlement") +def get_settlement( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """项目结算报告""" + project = db.query(Project).filter(Project.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + return calc_project_settlement(project_id, db) + + +@router.get("/projects/{project_id}/efficiency") +def get_efficiency( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """项目团队效率数据""" + return calc_team_efficiency(project_id, db) diff --git a/backend/routers/projects.py b/backend/routers/projects.py new file mode 100644 index 0000000..92c7a6a --- /dev/null +++ b/backend/routers/projects.py @@ -0,0 +1,158 @@ +"""项目管理路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from sqlalchemy import func as sa_func +from typing import List, Optional +from database import get_db +from models import ( + User, Project, Submission, UserRole, ProjectType, + ProjectStatus, PhaseGroup, WorkType +) +from schemas import ProjectCreate, ProjectUpdate, ProjectOut +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/projects", tags=["项目管理"]) + + +def enrich_project(p: Project, db: Session) -> ProjectOut: + """将项目对象转为带计算字段的输出""" + # 累计提交秒数(仅有秒数的提交) + total_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.total_seconds > 0 + ).scalar() or 0 + + target = p.target_total_seconds + progress = round(total_secs / target * 100, 1) if target > 0 else 0 + + # 损耗 = 测试损耗 + 超产损耗 + test_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.work_type == WorkType.TEST + ).scalar() or 0 + overproduction = max(0, total_secs - target) + waste = test_secs + overproduction + waste_rate = round(waste / target * 100, 1) if target > 0 else 0 + + leader_name = p.leader.name if p.leader else None + + return ProjectOut( + id=p.id, name=p.name, + project_type=p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, + status=p.status.value if hasattr(p.status, 'value') else p.status, + leader_id=p.leader_id, leader_name=leader_name, + current_phase=p.current_phase.value if hasattr(p.current_phase, 'value') else p.current_phase, + episode_duration_minutes=p.episode_duration_minutes, + episode_count=p.episode_count, + target_total_seconds=target, + estimated_completion_date=p.estimated_completion_date, + actual_completion_date=p.actual_completion_date, + contract_amount=p.contract_amount, + created_at=p.created_at, + total_submitted_seconds=round(total_secs, 1), + progress_percent=progress, + waste_seconds=round(waste, 1), + waste_rate=waste_rate, + ) + + +@router.get("/", response_model=List[ProjectOut]) +def list_projects( + status: Optional[str] = Query(None), + project_type: Optional[str] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + q = db.query(Project) + if status: + q = q.filter(Project.status == ProjectStatus(status)) + if project_type: + q = q.filter(Project.project_type == ProjectType(project_type)) + projects = q.order_by(Project.created_at.desc()).all() + return [enrich_project(p, db) for p in projects] + + +@router.post("/", response_model=ProjectOut) +def create_project( + req: ProjectCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + project = Project( + name=req.name, + project_type=ProjectType(req.project_type), + leader_id=req.leader_id, + current_phase=PhaseGroup(req.current_phase), + episode_duration_minutes=req.episode_duration_minutes, + episode_count=req.episode_count, + estimated_completion_date=req.estimated_completion_date, + contract_amount=req.contract_amount, + ) + db.add(project) + db.commit() + db.refresh(project) + return enrich_project(project, db) + + +@router.get("/{project_id}", response_model=ProjectOut) +def get_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + return enrich_project(p, db) + + +@router.put("/{project_id}", response_model=ProjectOut) +def update_project( + project_id: int, + req: ProjectUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR)) +): + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + if req.name is not None: + p.name = req.name + if req.project_type is not None: + p.project_type = ProjectType(req.project_type) + if req.status is not None: + p.status = ProjectStatus(req.status) + if req.leader_id is not None: + p.leader_id = req.leader_id + if req.current_phase is not None: + p.current_phase = PhaseGroup(req.current_phase) + if req.episode_duration_minutes is not None: + p.episode_duration_minutes = req.episode_duration_minutes + if req.episode_count is not None: + p.episode_count = req.episode_count + if req.estimated_completion_date is not None: + p.estimated_completion_date = req.estimated_completion_date + if req.actual_completion_date is not None: + p.actual_completion_date = req.actual_completion_date + if req.contract_amount is not None: + p.contract_amount = req.contract_amount + db.commit() + db.refresh(p) + return enrich_project(p, db) + + +@router.post("/{project_id}/complete") +def complete_project( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + """Owner 手动确认项目完成""" + p = db.query(Project).filter(Project.id == project_id).first() + if not p: + raise HTTPException(status_code=404, detail="项目不存在") + from datetime import date + p.status = ProjectStatus.COMPLETED + p.actual_completion_date = date.today() + db.commit() + return {"message": "项目已标记为完成", "project_id": project_id} diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py new file mode 100644 index 0000000..7d937da --- /dev/null +++ b/backend/routers/submissions.py @@ -0,0 +1,193 @@ +"""内容提交路由""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List, Optional +from datetime import date +from database import get_db +from models import ( + User, Submission, SubmissionHistory, Project, UserRole, + PhaseGroup, WorkType, ContentType, SubmitTo +) +from schemas import SubmissionCreate, SubmissionUpdate, SubmissionOut +from auth import get_current_user, require_role + +router = APIRouter(prefix="/api/submissions", tags=["内容提交"]) + + +def submission_to_out(s: Submission) -> SubmissionOut: + return SubmissionOut( + id=s.id, user_id=s.user_id, + user_name=s.user.name if s.user else None, + project_id=s.project_id, + project_name=s.project.name if s.project else None, + project_phase=s.project_phase.value if hasattr(s.project_phase, 'value') else s.project_phase, + work_type=s.work_type.value if hasattr(s.work_type, 'value') else s.work_type, + content_type=s.content_type.value if hasattr(s.content_type, 'value') else s.content_type, + duration_minutes=s.duration_minutes, + duration_seconds=s.duration_seconds, + total_seconds=s.total_seconds, + hours_spent=s.hours_spent, + submit_to=s.submit_to.value if hasattr(s.submit_to, 'value') else s.submit_to, + description=s.description, + submit_date=s.submit_date, + created_at=s.created_at, + ) + + +@router.get("/", response_model=List[SubmissionOut]) +def list_submissions( + project_id: Optional[int] = Query(None), + user_id: Optional[int] = Query(None), + start_date: Optional[date] = Query(None), + end_date: Optional[date] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + q = db.query(Submission) + # 成员只能看自己的 + if current_user.role == UserRole.MEMBER: + q = q.filter(Submission.user_id == current_user.id) + elif user_id: + q = q.filter(Submission.user_id == user_id) + if project_id: + q = q.filter(Submission.project_id == project_id) + if start_date: + q = q.filter(Submission.submit_date >= start_date) + if end_date: + q = q.filter(Submission.submit_date <= end_date) + subs = q.order_by(Submission.submit_date.desc(), Submission.created_at.desc()).all() + return [submission_to_out(s) for s in subs] + + +@router.post("/", response_model=SubmissionOut) +def create_submission( + req: SubmissionCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + # 校验项目存在 + project = db.query(Project).filter(Project.id == req.project_id).first() + if not project: + raise HTTPException(status_code=404, detail="项目不存在") + + # 自动计算总秒数 + total_seconds = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0) + + sub = Submission( + user_id=current_user.id, + project_id=req.project_id, + project_phase=PhaseGroup(req.project_phase), + work_type=WorkType(req.work_type), + content_type=ContentType(req.content_type), + duration_minutes=req.duration_minutes or 0, + duration_seconds=req.duration_seconds or 0, + total_seconds=total_seconds, + hours_spent=req.hours_spent, + submit_to=SubmitTo(req.submit_to), + description=req.description, + submit_date=req.submit_date, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return submission_to_out(sub) + + +@router.put("/{submission_id}", response_model=SubmissionOut) +def update_submission( + submission_id: int, + req: SubmissionUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """高权限修改提交记录(需填写原因)""" + sub = db.query(Submission).filter(Submission.id == submission_id).first() + if not sub: + raise HTTPException(status_code=404, detail="提交记录不存在") + + # 保存旧数据用于历史记录 + old_data = { + "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, + "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, + "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, + "duration_minutes": sub.duration_minutes, + "duration_seconds": sub.duration_seconds, + "total_seconds": sub.total_seconds, + "hours_spent": sub.hours_spent, + "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, + "description": sub.description, + "submit_date": str(sub.submit_date), + } + + # 更新字段 + if req.project_phase is not None: + sub.project_phase = PhaseGroup(req.project_phase) + if req.work_type is not None: + sub.work_type = WorkType(req.work_type) + if req.content_type is not None: + sub.content_type = ContentType(req.content_type) + if req.duration_minutes is not None: + sub.duration_minutes = req.duration_minutes + if req.duration_seconds is not None: + sub.duration_seconds = req.duration_seconds + if req.hours_spent is not None: + sub.hours_spent = req.hours_spent + if req.submit_to is not None: + sub.submit_to = SubmitTo(req.submit_to) + if req.description is not None: + sub.description = req.description + if req.submit_date is not None: + sub.submit_date = req.submit_date + + # 重算总秒数 + sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0) + + # 保存新数据 + new_data = { + "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, + "work_type": sub.work_type.value if hasattr(sub.work_type, 'value') else sub.work_type, + "content_type": sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type, + "duration_minutes": sub.duration_minutes, + "duration_seconds": sub.duration_seconds, + "total_seconds": sub.total_seconds, + "hours_spent": sub.hours_spent, + "submit_to": sub.submit_to.value if hasattr(sub.submit_to, 'value') else sub.submit_to, + "description": sub.description, + "submit_date": str(sub.submit_date), + } + + # 写入修改历史 + history = SubmissionHistory( + submission_id=sub.id, + changed_by=current_user.id, + change_reason=req.change_reason, + old_data=old_data, + new_data=new_data, + ) + db.add(history) + db.commit() + db.refresh(sub) + return submission_to_out(sub) + + +@router.get("/{submission_id}/history") +def get_submission_history( + submission_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + """查看提交的修改历史""" + records = db.query(SubmissionHistory).filter( + SubmissionHistory.submission_id == submission_id + ).order_by(SubmissionHistory.created_at.desc()).all() + return [ + { + "id": r.id, + "changed_by": r.changed_by, + "change_reason": r.change_reason, + "old_data": r.old_data, + "new_data": r.new_data, + "created_at": r.created_at, + } + for r in records + ] diff --git a/backend/routers/users.py b/backend/routers/users.py new file mode 100644 index 0000000..5435975 --- /dev/null +++ b/backend/routers/users.py @@ -0,0 +1,88 @@ +"""用户管理路由""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from database import get_db +from models import User, UserRole, PhaseGroup +from schemas import UserCreate, UserUpdate, UserOut +from auth import get_current_user, hash_password, require_role + +router = APIRouter(prefix="/api/users", tags=["用户管理"]) + + +def user_to_out(u: User) -> UserOut: + return UserOut( + id=u.id, username=u.username, name=u.name, + phase_group=u.phase_group.value if hasattr(u.phase_group, 'value') else u.phase_group, + role=u.role.value if hasattr(u.role, 'value') else u.role, + monthly_salary=u.monthly_salary, daily_cost=u.daily_cost, + is_active=u.is_active, created_at=u.created_at, + ) + + +@router.get("/", response_model=List[UserOut]) +def list_users( + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER)) +): + users = db.query(User).order_by(User.created_at.desc()).all() + return [user_to_out(u) for u in users] + + +@router.post("/", response_model=UserOut) +def create_user( + req: UserCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + if db.query(User).filter(User.username == req.username).first(): + raise HTTPException(status_code=400, detail="用户名已存在") + user = User( + username=req.username, + password_hash=hash_password(req.password), + name=req.name, + phase_group=PhaseGroup(req.phase_group), + role=UserRole(req.role), + monthly_salary=req.monthly_salary, + ) + db.add(user) + db.commit() + db.refresh(user) + return user_to_out(user) + + +@router.put("/{user_id}", response_model=UserOut) +def update_user( + user_id: int, + req: UserUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_role(UserRole.OWNER)) +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + if req.name is not None: + user.name = req.name + if req.phase_group is not None: + user.phase_group = PhaseGroup(req.phase_group) + if req.role is not None: + user.role = UserRole(req.role) + if req.monthly_salary is not None: + user.monthly_salary = req.monthly_salary + if req.is_active is not None: + user.is_active = req.is_active + db.commit() + db.refresh(user) + return user_to_out(user) + + +@router.get("/{user_id}", response_model=UserOut) +def get_user( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + return user_to_out(user) diff --git a/backend/schemas.py b/backend/schemas.py new file mode 100644 index 0000000..9fef907 --- /dev/null +++ b/backend/schemas.py @@ -0,0 +1,249 @@ +"""Pydantic 数据模型 —— API 请求/响应格式定义""" +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import date, datetime + + +# ──────────────────────────── 认证 ──────────────────────────── + +class LoginRequest(BaseModel): + username: str + password: str + + +class Token(BaseModel): + access_token: str + token_type: str = "bearer" + + +# ──────────────────────────── 用户 ──────────────────────────── + +class UserCreate(BaseModel): + username: str + password: str + name: str + phase_group: str # 前期/制作/后期 + role: str = "成员" + monthly_salary: float = 0 + + +class UserUpdate(BaseModel): + name: Optional[str] = None + phase_group: Optional[str] = None + role: Optional[str] = None + monthly_salary: Optional[float] = None + is_active: Optional[int] = None + + +class UserOut(BaseModel): + id: int + username: str + name: str + phase_group: str + role: str + monthly_salary: float + daily_cost: float + is_active: int + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 项目 ──────────────────────────── + +class ProjectCreate(BaseModel): + name: str + project_type: str + leader_id: Optional[int] = None + current_phase: str = "前期" + episode_duration_minutes: float + episode_count: int + estimated_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + project_type: Optional[str] = None + status: Optional[str] = None + leader_id: Optional[int] = None + current_phase: Optional[str] = None + episode_duration_minutes: Optional[float] = None + episode_count: Optional[int] = None + estimated_completion_date: Optional[date] = None + actual_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + + +class ProjectOut(BaseModel): + id: int + name: str + project_type: str + status: str + leader_id: Optional[int] = None + leader_name: Optional[str] = None + current_phase: str + episode_duration_minutes: float + episode_count: int + target_total_seconds: int + estimated_completion_date: Optional[date] = None + actual_completion_date: Optional[date] = None + contract_amount: Optional[float] = None + created_at: Optional[datetime] = None + # 动态计算字段 + total_submitted_seconds: Optional[float] = 0 + progress_percent: Optional[float] = 0 + waste_seconds: Optional[float] = 0 + waste_rate: Optional[float] = 0 + + class Config: + from_attributes = True + + +# ──────────────────────────── 内容提交 ──────────────────────────── + +class SubmissionCreate(BaseModel): + project_id: int + project_phase: str + work_type: str + content_type: str + duration_minutes: Optional[float] = 0 + duration_seconds: Optional[float] = 0 + hours_spent: Optional[float] = None + submit_to: str + description: Optional[str] = None + submit_date: date + + +class SubmissionUpdate(BaseModel): + project_phase: Optional[str] = None + work_type: Optional[str] = None + content_type: Optional[str] = None + duration_minutes: Optional[float] = None + duration_seconds: Optional[float] = None + hours_spent: Optional[float] = None + submit_to: Optional[str] = None + description: Optional[str] = None + submit_date: Optional[date] = None + change_reason: str # 修改必须填原因 + + +class SubmissionOut(BaseModel): + id: int + user_id: int + user_name: Optional[str] = None + project_id: int + project_name: Optional[str] = None + project_phase: str + work_type: str + content_type: str + duration_minutes: Optional[float] = 0 + duration_seconds: Optional[float] = 0 + total_seconds: float + hours_spent: Optional[float] = None + submit_to: str + description: Optional[str] = None + submit_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── AI 工具成本 ──────────────────────────── + +class AIToolCostCreate(BaseModel): + tool_name: str + subscription_period: str + amount: float + allocation_type: str + project_id: Optional[int] = None + record_date: date + allocations: Optional[List[dict]] = None # [{project_id, percentage}] + + +class AIToolCostOut(BaseModel): + id: int + tool_name: str + subscription_period: str + amount: float + allocation_type: str + project_id: Optional[int] = None + recorded_by: int + record_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 外包成本 ──────────────────────────── + +class OutsourceCostCreate(BaseModel): + project_id: int + outsource_type: str + episode_start: Optional[int] = None + episode_end: Optional[int] = None + amount: float + record_date: date + + +class OutsourceCostOut(BaseModel): + id: int + project_id: int + outsource_type: str + episode_start: Optional[int] = None + episode_end: Optional[int] = None + amount: float + recorded_by: int + record_date: date + created_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +# ──────────────────────────── 成本调整 ──────────────────────────── + +class CostOverrideCreate(BaseModel): + user_id: int + date: date + project_id: int + override_amount: float + reason: Optional[str] = None + + +# ──────────────────────────── 仪表盘 ──────────────────────────── + +class DashboardSummary(BaseModel): + """全局仪表盘数据""" + active_projects: int = 0 + completed_projects: int = 0 + monthly_labor_cost: float = 0 + monthly_ai_tool_cost: float = 0 + monthly_total_seconds: float = 0 + avg_daily_seconds_per_person: float = 0 + projects: List[dict] = [] + waste_ranking: List[dict] = [] + settled_projects: List[dict] = [] + + +# ──────────────────────────── 项目结算 ──────────────────────────── + +class SettlementOut(BaseModel): + """项目结算报告""" + project_id: int + project_name: str + project_type: str + labor_cost: float = 0 + ai_tool_cost: float = 0 + outsource_cost: float = 0 + total_cost: float = 0 + waste_seconds: float = 0 + waste_rate: float = 0 + test_waste_seconds: float = 0 + overproduction_waste_seconds: float = 0 + contract_amount: Optional[float] = None + profit_loss: Optional[float] = None + team_efficiency: List[dict] = [] diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..cd722ae --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,195 @@ +"""种子数据 —— 用于测试和演示""" +from datetime import date, timedelta +from database import SessionLocal, engine, Base +from models import * +from auth import hash_password + +Base.metadata.create_all(bind=engine) +db = SessionLocal() + + +def seed(): + # 清空数据 + for table in reversed(Base.metadata.sorted_tables): + db.execute(table.delete()) + db.commit() + + # ── 用户 ── + users = [ + User(username="admin", password_hash=hash_password("admin123"), + name="老板", phase_group=PhaseGroup.PRODUCTION, role=UserRole.OWNER, monthly_salary=0), + User(username="zhangsan", password_hash=hash_password("123456"), + name="张三", phase_group=PhaseGroup.PRE, role=UserRole.LEADER, monthly_salary=15000), + User(username="lisi", password_hash=hash_password("123456"), + name="李四", phase_group=PhaseGroup.PRODUCTION, role=UserRole.LEADER, monthly_salary=18000), + User(username="wangwu", password_hash=hash_password("123456"), + name="王五", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000), + User(username="zhaoliu", password_hash=hash_password("123456"), + name="赵六", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000), + User(username="sunqi", password_hash=hash_password("123456"), + name="孙七", phase_group=PhaseGroup.POST, role=UserRole.MEMBER, monthly_salary=13000), + User(username="producer", password_hash=hash_password("123456"), + name="陈制片", phase_group=PhaseGroup.PRODUCTION, role=UserRole.SUPERVISOR, monthly_salary=20000), + ] + db.add_all(users) + db.flush() + + admin, zhangsan, lisi, wangwu, zhaoliu, sunqi, producer = users + + # ── 项目 ── + proj_a = Project( + name="星际漫游 第一季", project_type=ProjectType.CLIENT_FORMAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRODUCTION, + episode_duration_minutes=5, episode_count=13, + estimated_completion_date=date.today() + timedelta(days=60), + contract_amount=100000, + ) + proj_b = Project( + name="品牌方 TVC", project_type=ProjectType.CLIENT_FORMAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRODUCTION, + episode_duration_minutes=1, episode_count=3, + estimated_completion_date=date.today() + timedelta(days=20), + contract_amount=50000, + ) + proj_c = Project( + name="甲方风格测试", project_type=ProjectType.CLIENT_TEST, + leader_id=zhangsan.id, current_phase=PhaseGroup.PRE, + episode_duration_minutes=1, episode_count=1, + ) + proj_d = Project( + name="AI 短剧原创", project_type=ProjectType.INTERNAL_ORIGINAL, + leader_id=lisi.id, current_phase=PhaseGroup.PRE, + episode_duration_minutes=8, episode_count=6, + estimated_completion_date=date.today() + timedelta(days=90), + ) + db.add_all([proj_a, proj_b, proj_c, proj_d]) + db.flush() + + # ── 内容提交(模拟近两周的数据) ── + base_date = date.today() - timedelta(days=14) + submissions = [] + + # 张三(前期组)给项目 A 和 D 做前期 + for i in range(5): + d = base_date + timedelta(days=i) + submissions.append(Submission( + user_id=zhangsan.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRE, work_type=WorkType.PLAN, + content_type=ContentType.DESIGN, total_seconds=0, + submit_to=SubmitTo.INTERNAL, description=f"角色设定第{i+1}版", + submit_date=d, + )) + for i in range(3): + d = base_date + timedelta(days=i + 5) + submissions.append(Submission( + user_id=zhangsan.id, project_id=proj_d.id, + project_phase=PhaseGroup.PRE, work_type=WorkType.PLAN, + content_type=ContentType.DESIGN, total_seconds=0, + submit_to=SubmitTo.INTERNAL, description=f"剧本大纲第{i+1}稿", + submit_date=d, + )) + + # 李四(制作组组长)主要做项目 A + for i in range(10): + d = base_date + timedelta(days=i) + secs = 45 + (i % 3) * 15 # 45-75秒 + wt = WorkType.TEST if i < 2 else WorkType.PRODUCTION + submissions.append(Submission( + user_id=lisi.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=wt, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.INTERNAL, description=f"第1集片段 - 场景{i+1}", + submit_date=d, + )) + + # 王五(制作组)做项目 A 和 B + for i in range(8): + d = base_date + timedelta(days=i) + secs = 30 + (i % 4) * 20 # 30-90秒 + submissions.append(Submission( + user_id=wangwu.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"第2集动画片段{i+1}", + submit_date=d, + )) + for i in range(4): + d = base_date + timedelta(days=i + 8) + secs = 20 + i * 10 + submissions.append(Submission( + user_id=wangwu.id, project_id=proj_b.id, + project_phase=PhaseGroup.PRODUCTION, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"TVC 片段{i+1}", + submit_date=d, + )) + + # 赵六(制作组)做项目 A + for i in range(10): + d = base_date + timedelta(days=i) + secs = 50 + (i % 2) * 30 # 50-80秒 + wt = WorkType.TEST if i < 1 else WorkType.PRODUCTION + submissions.append(Submission( + user_id=zhaoliu.id, project_id=proj_a.id, + project_phase=PhaseGroup.PRODUCTION, work_type=wt, + content_type=ContentType.ANIMATION, total_seconds=secs, + duration_minutes=secs // 60, duration_seconds=secs % 60, + submit_to=SubmitTo.LEADER, description=f"第3集场景动画{i+1}", + submit_date=d, + )) + + # 孙七(后期组)剪辑 + for i in range(3): + d = base_date + timedelta(days=i + 10) + submissions.append(Submission( + user_id=sunqi.id, project_id=proj_a.id, + project_phase=PhaseGroup.POST, work_type=WorkType.PRODUCTION, + content_type=ContentType.EDITING, total_seconds=0, + submit_to=SubmitTo.PRODUCER, description=f"第{i+1}集粗剪完成", + submit_date=d, + )) + # 后期补拍 + submissions.append(Submission( + user_id=sunqi.id, project_id=proj_a.id, + project_phase=PhaseGroup.POST, work_type=WorkType.PRODUCTION, + content_type=ContentType.ANIMATION, total_seconds=15, + duration_seconds=15, + submit_to=SubmitTo.PRODUCER, description="第1集补拍修改镜头", + submit_date=base_date + timedelta(days=12), + )) + + db.add_all(submissions) + + # ── AI 工具成本 ── + db.add(AIToolCost( + tool_name="Midjourney", subscription_period=SubscriptionPeriod.MONTHLY, + amount=200, allocation_type=CostAllocationType.TEAM, + recorded_by=producer.id, record_date=date.today().replace(day=1), + )) + db.add(AIToolCost( + tool_name="Runway", subscription_period=SubscriptionPeriod.MONTHLY, + amount=600, allocation_type=CostAllocationType.PROJECT, + project_id=proj_a.id, + recorded_by=producer.id, record_date=date.today().replace(day=1), + )) + + # ── 外包成本 ── + db.add(OutsourceCost( + project_id=proj_a.id, outsource_type=OutsourceType.ANIMATION, + episode_start=10, episode_end=13, amount=20000, + recorded_by=producer.id, record_date=date.today() - timedelta(days=5), + )) + + db.commit() + print("[OK] seed data generated") + print(f" - users: {len(users)}") + print(f" - projects: 4") + print(f" - submissions: {len(submissions)}") + print(f" - default account: admin / admin123") + + +if __name__ == "__main__": + seed() diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..1511959 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..97d02f3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2041 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.5", + "echarts": "^6.0.0", + "element-plus": "^2.13.2", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.2", + "vite": "^7.3.1" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "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": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "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/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "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.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "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", + "peer": true, + "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": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/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/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/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/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.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "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/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/element-plus": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.2.tgz", + "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@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": "^10.11.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" + }, + "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.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT", + "peer": true + }, + "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/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "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/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "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/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "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.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "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-router/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/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..bb931c6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.5", + "echarts": "^6.0.0", + "element-plus": "^2.13.2", + "pinia": "^3.0.4", + "vue": "^3.5.25", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.2", + "vite": "^7.3.1" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..6d0ab4d --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..3ab61a7 --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,85 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '../router' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000, +}) + +// 请求拦截:自动带 token +api.interceptors.request.use(config => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 响应拦截:统一错误处理 +api.interceptors.response.use( + res => res.data, + err => { + const msg = err.response?.data?.detail || '请求失败' + if (err.response?.status === 401) { + localStorage.removeItem('token') + router.push('/login') + ElMessage.error('登录已过期,请重新登录') + } else { + ElMessage.error(msg) + } + return Promise.reject(err) + } +) + +export default api + +// ── 认证 ── +export const authApi = { + login: (data) => api.post('/auth/login', data), + me: () => api.get('/auth/me'), +} + +// ── 用户 ── +export const userApi = { + list: () => api.get('/users/'), + create: (data) => api.post('/users/', data), + update: (id, data) => api.put(`/users/${id}`, data), + get: (id) => api.get(`/users/${id}`), +} + +// ── 项目 ── +export const projectApi = { + list: (params) => api.get('/projects/', { params }), + create: (data) => api.post('/projects/', data), + update: (id, data) => api.put(`/projects/${id}`, data), + get: (id) => api.get(`/projects/${id}`), + complete: (id) => api.post(`/projects/${id}/complete`), + settlement: (id) => api.get(`/projects/${id}/settlement`), + efficiency: (id) => api.get(`/projects/${id}/efficiency`), +} + +// ── 内容提交 ── +export const submissionApi = { + list: (params) => api.get('/submissions/', { params }), + create: (data) => api.post('/submissions/', data), + update: (id, data) => api.put(`/submissions/${id}`, data), + history: (id) => api.get(`/submissions/${id}/history`), +} + +// ── 成本 ── +export const costApi = { + listAITools: () => api.get('/costs/ai-tools'), + createAITool: (data) => api.post('/costs/ai-tools', data), + deleteAITool: (id) => api.delete(`/costs/ai-tools/${id}`), + listOutsource: (params) => api.get('/costs/outsource', { params }), + createOutsource: (data) => api.post('/costs/outsource', data), + deleteOutsource: (id) => api.delete(`/costs/outsource/${id}`), + createOverride: (data) => api.post('/costs/overrides', data), + listOverrides: (params) => api.get('/costs/overrides', { params }), +} + +// ── 仪表盘 ── +export const dashboardApi = { + get: () => api.get('/dashboard'), +} diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ 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..546ebbc --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue new file mode 100644 index 0000000..8f7100d --- /dev/null +++ b/frontend/src/components/Layout.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..2de1c69 --- /dev/null +++ b/frontend/src/main.js @@ -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(ElementPlus, { locale: zhCn }) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..bdc8b60 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,38 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { path: '/login', name: 'Login', component: () => import('../views/Login.vue'), meta: { public: true } }, + { + path: '/', + component: () => import('../components/Layout.vue'), + redirect: '/dashboard', + children: [ + { path: 'dashboard', name: 'Dashboard', component: () => import('../views/Dashboard.vue'), meta: { roles: ['Owner'] } }, + { path: 'projects', name: 'Projects', component: () => import('../views/Projects.vue') }, + { path: 'projects/:id', name: 'ProjectDetail', component: () => import('../views/ProjectDetail.vue') }, + { path: 'submissions', name: 'Submissions', component: () => import('../views/Submissions.vue') }, + { path: 'costs', name: 'Costs', component: () => import('../views/Costs.vue'), meta: { roles: ['Owner', '主管', '组长'] } }, + { path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { roles: ['Owner'] } }, + { path: 'settlement/:id', name: 'Settlement', component: () => import('../views/Settlement.vue'), meta: { roles: ['Owner'] } }, + ], + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// 路由守卫 +router.beforeEach(async (to, from, next) => { + const token = localStorage.getItem('token') + if (to.meta.public) { + next() + } else if (!token) { + next('/login') + } else { + next() + } +}) + +export default router diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js new file mode 100644 index 0000000..e97d866 --- /dev/null +++ b/frontend/src/stores/auth.js @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { authApi } from '../api' + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const token = ref(localStorage.getItem('token') || '') + + async function login(username, password) { + const res = await authApi.login({ username, password }) + token.value = res.access_token + localStorage.setItem('token', res.access_token) + await fetchUser() + } + + async function fetchUser() { + try { + user.value = await authApi.me() + } catch { + logout() + } + } + + function logout() { + user.value = null + token.value = '' + localStorage.removeItem('token') + } + + const isOwner = () => user.value?.role === 'Owner' + const isSupervisor = () => ['Owner', '主管'].includes(user.value?.role) + const isLeaderOrAbove = () => ['Owner', '主管', '组长'].includes(user.value?.role) + + return { user, token, login, fetchUser, logout, isOwner, isSupervisor, isLeaderOrAbove } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..f691315 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/views/Costs.vue b/frontend/src/views/Costs.vue new file mode 100644 index 0000000..14c6bd8 --- /dev/null +++ b/frontend/src/views/Costs.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..8b1827d --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..fc94a0b --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue new file mode 100644 index 0000000..33ee2e3 --- /dev/null +++ b/frontend/src/views/ProjectDetail.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/views/Projects.vue b/frontend/src/views/Projects.vue new file mode 100644 index 0000000..185a6aa --- /dev/null +++ b/frontend/src/views/Projects.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/frontend/src/views/Settlement.vue b/frontend/src/views/Settlement.vue new file mode 100644 index 0000000..ff88d83 --- /dev/null +++ b/frontend/src/views/Settlement.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue new file mode 100644 index 0000000..e9fc64b --- /dev/null +++ b/frontend/src/views/Submissions.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue new file mode 100644 index 0000000..618701c --- /dev/null +++ b/frontend/src/views/Users.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..d62dc96 --- /dev/null +++ b/frontend/vite.config.js @@ -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, + } + } + } +})