diff --git a/backend/airlabs.db b/backend/airlabs.db
index 1ceb16a..6ff9f2e 100644
Binary files a/backend/airlabs.db and b/backend/airlabs.db differ
diff --git a/backend/auth.py b/backend/auth.py
index 7820778..9be5348 100644
--- a/backend/auth.py
+++ b/backend/auth.py
@@ -7,7 +7,6 @@ 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")
@@ -29,8 +28,9 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
-def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
+def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
"""从 JWT token 解析当前用户"""
+ from models import User
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="登录已过期,请重新登录",
@@ -51,13 +51,13 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(
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="权限不足"
- )
+def require_permission(*perms: str):
+ """权限校验依赖:要求当前用户拥有指定权限中的至少一项"""
+ def perm_checker(current_user=Depends(get_current_user)):
+ if not current_user.role_ref:
+ raise HTTPException(status_code=403, detail="未分配角色")
+ user_perms = current_user.permissions or []
+ if not any(p in user_perms for p in perms):
+ raise HTTPException(status_code=403, detail="权限不足")
return current_user
- return role_checker
+ return perm_checker
diff --git a/backend/calculations.py b/backend/calculations.py
index c678849..9a41c25 100644
--- a/backend/calculations.py
+++ b/backend/calculations.py
@@ -8,7 +8,7 @@ from collections import defaultdict
from datetime import date, timedelta
from models import (
User, Project, Submission, AIToolCost, AIToolCostAllocation,
- OutsourceCost, CostOverride, WorkType, CostAllocationType
+ OutsourceCost, CostOverride, OverheadCost, WorkType, CostAllocationType
)
from config import WORKING_DAYS_PER_MONTH
@@ -142,6 +142,31 @@ def calc_outsource_cost_for_project(project_id: int, db: Session) -> float:
return round(total, 2)
+# ──────────────────────────── 固定开支分摊 ────────────────────────────
+
+def calc_overhead_cost_for_project(project_id: int, db: Session) -> float:
+ """
+ 计算某项目分摊的固定开支(办公室租金+水电费)
+ 规则:按所有项目的产出秒数比例均摊
+ """
+ total_overhead = db.query(sa_func.sum(OverheadCost.amount)).scalar() or 0
+ if total_overhead == 0:
+ return 0.0
+
+ 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
+ return round(total_overhead * ratio, 2)
+ return 0.0
+
+
# ──────────────────────────── 损耗计算 ────────────────────────────
def calc_waste_for_project(project_id: int, db: Session) -> dict:
@@ -243,7 +268,8 @@ def calc_project_settlement(project_id: int, db: Session) -> dict:
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
+ overhead = calc_overhead_cost_for_project(project_id, db)
+ total_cost = labor + ai_tool + outsource + overhead
waste = calc_waste_for_project(project_id, db)
efficiency = calc_team_efficiency(project_id, db)
@@ -254,6 +280,7 @@ def calc_project_settlement(project_id: int, db: Session) -> dict:
"labor_cost": labor,
"ai_tool_cost": ai_tool,
"outsource_cost": outsource,
+ "overhead_cost": overhead,
"total_cost": round(total_cost, 2),
**waste,
"team_efficiency": efficiency,
diff --git a/backend/main.py b/backend/main.py
index 58e34dd..28754ea 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -4,7 +4,7 @@ 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 models import User, Role, PhaseGroup, BUILTIN_ROLES
from auth import hash_password
import os
@@ -30,6 +30,7 @@ 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
+from routers.roles import router as roles_router
app.include_router(auth_router)
app.include_router(users_router)
@@ -37,6 +38,7 @@ app.include_router(projects_router)
app.include_router(submissions_router)
app.include_router(costs_router)
app.include_router(dashboard_router)
+app.include_router(roles_router)
# 前端静态文件
frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
@@ -52,18 +54,34 @@ if os.path.exists(frontend_dir):
@app.on_event("startup")
-def init_default_owner():
- """首次启动时创建默认 Owner 账号"""
+def init_roles_and_admin():
+ """首次启动时创建内置角色和默认管理员"""
from database import SessionLocal
db = SessionLocal()
try:
- if not db.query(User).filter(User.role == UserRole.OWNER).first():
+ # 初始化内置角色
+ for role_name, role_def in BUILTIN_ROLES.items():
+ existing = db.query(Role).filter(Role.name == role_name).first()
+ if not existing:
+ role = Role(
+ name=role_name,
+ description=role_def["description"],
+ permissions=role_def["permissions"],
+ is_system=1,
+ )
+ db.add(role)
+ print(f"[OK] created role: {role_name}")
+ db.commit()
+
+ # 创建默认管理员(关联超级管理员角色)
+ admin_role = db.query(Role).filter(Role.name == "超级管理员").first()
+ if admin_role and not db.query(User).filter(User.username == "admin").first():
owner = User(
username="admin",
password_hash=hash_password("admin123"),
name="管理员",
phase_group=PhaseGroup.PRODUCTION,
- role=UserRole.OWNER,
+ role_id=admin_role.id,
monthly_salary=0,
)
db.add(owner)
diff --git a/backend/models.py b/backend/models.py
index ab32cb1..c091d64 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -9,6 +9,71 @@ from database import Base
import enum
+# ──────────────────────────── 权限标识符定义 ────────────────────────────
+
+ALL_PERMISSIONS = [
+ # 仪表盘
+ ("dashboard:view", "查看仪表盘", "仪表盘"),
+ # 项目管理
+ ("project:view", "查看项目", "项目管理"),
+ ("project:create", "创建项目", "项目管理"),
+ ("project:edit", "编辑项目", "项目管理"),
+ ("project:delete", "删除项目", "项目管理"),
+ ("project:complete", "确认完成项目", "项目管理"),
+ # 内容提交
+ ("submission:view", "查看提交记录", "内容提交"),
+ ("submission:create", "新增提交", "内容提交"),
+ # 成本管理
+ ("cost:view", "查看成本", "成本管理"),
+ ("cost:create", "录入成本", "成本管理"),
+ ("cost:delete", "删除成本", "成本管理"),
+ # 用户与角色
+ ("user:view", "查看用户列表", "用户与角色"),
+ ("user:manage", "管理用户", "用户与角色"),
+ ("role:manage", "管理角色", "用户与角色"),
+ # 结算与效率
+ ("settlement:view", "查看结算报告", "结算与效率"),
+ ("efficiency:view", "查看团队效率", "结算与效率"),
+]
+
+PERMISSION_KEYS = [p[0] for p in ALL_PERMISSIONS]
+
+# 内置角色定义
+BUILTIN_ROLES = {
+ "超级管理员": {
+ "description": "系统最高权限,拥有全部功能",
+ "permissions": PERMISSION_KEYS[:], # 全部
+ },
+ "主管": {
+ "description": "管理项目和成本,不可管理用户和角色",
+ "permissions": [
+ "dashboard:view",
+ "project:view", "project:create", "project:edit", "project:complete",
+ "submission:view", "submission:create",
+ "cost:view", "cost:create", "cost:delete",
+ "user:view",
+ "settlement:view", "efficiency:view",
+ ],
+ },
+ "组长": {
+ "description": "管理本组提交和查看成本",
+ "permissions": [
+ "project:view", "project:create",
+ "submission:view", "submission:create",
+ "cost:view", "cost:create",
+ "efficiency:view",
+ ],
+ },
+ "成员": {
+ "description": "提交内容和查看项目",
+ "permissions": [
+ "project:view",
+ "submission:view", "submission:create",
+ ],
+ },
+}
+
+
# ──────────────────────────── 枚举定义 ────────────────────────────
class ProjectType(str, enum.Enum):
@@ -29,13 +94,6 @@ class PhaseGroup(str, enum.Enum):
POST = "后期"
-class UserRole(str, enum.Enum):
- MEMBER = "成员"
- LEADER = "组长"
- SUPERVISOR = "主管"
- OWNER = "Owner"
-
-
class WorkType(str, enum.Enum):
PRODUCTION = "制作"
TEST = "测试"
@@ -73,6 +131,29 @@ class OutsourceType(str, enum.Enum):
FULL_EPISODE = "整集"
+class OverheadCostType(str, enum.Enum):
+ OFFICE_RENT = "办公室租金"
+ UTILITIES = "水电费"
+
+
+# ──────────────────────────── 角色 ────────────────────────────
+
+class Role(Base):
+ __tablename__ = "roles"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(50), unique=True, nullable=False)
+ description = Column(String(200), nullable=True)
+ permissions = Column(JSON, nullable=False, default=[]) # 权限标识符列表
+ is_system = Column(Integer, nullable=False, default=0) # 1=内置角色不可删
+ created_at = Column(DateTime, server_default=func.now())
+
+ users = relationship("User", back_populates="role_ref")
+
+ def has_permission(self, perm: str) -> bool:
+ return perm in (self.permissions or [])
+
+
# ──────────────────────────── 用户 ────────────────────────────
class User(Base):
@@ -83,20 +164,37 @@ class User(Base):
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)
+ role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
monthly_salary = Column(Float, nullable=False, default=0)
+ bonus = Column(Float, nullable=False, default=0)
+ social_insurance = Column(Float, nullable=False, default=0)
is_active = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, server_default=func.now())
# 关系
+ role_ref = relationship("Role", back_populates="users")
submissions = relationship("Submission", back_populates="user")
led_projects = relationship("Project", back_populates="leader")
+ @property
+ def role_name(self):
+ return self.role_ref.name if self.role_ref else ""
+
+ @property
+ def permissions(self):
+ return self.role_ref.permissions if self.role_ref else []
+
+ def has_permission(self, perm: str) -> bool:
+ return self.role_ref.has_permission(perm) if self.role_ref else False
+
+ @property
+ def monthly_total_cost(self):
+ return (self.monthly_salary or 0) + (self.bonus or 0) + (self.social_insurance or 0)
+
@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
+ return round(self.monthly_total_cost / WORKING_DAYS_PER_MONTH, 2) if self.monthly_total_cost else 0
# ──────────────────────────── 项目 ────────────────────────────
@@ -114,10 +212,9 @@ class Project(Base):
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) # 仅客户正式项目
+ 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")
@@ -125,7 +222,6 @@ class Project(Base):
@property
def target_total_seconds(self):
- """目标总秒数 = 单集时长(分) × 60 × 集数"""
return int(self.episode_duration_minutes * 60 * self.episode_count)
@@ -142,14 +238,13 @@ class Submission(Base):
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) # 可选:投入时长(小时)
+ 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")
@@ -165,23 +260,21 @@ class AIToolCost(Base):
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) # 指定项目时
+ 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
+ percentage = Column(Float, nullable=False)
ai_tool_cost = relationship("AIToolCost", back_populates="allocations")
project = relationship("Project", back_populates="ai_tool_allocations")
@@ -208,7 +301,6 @@ class OutsourceCost(Base):
# ──────────────────────────── 人力成本手动调整 ────────────────────────────
class CostOverride(Base):
- """管理员手动修改某人某天的成本分摊"""
__tablename__ = "cost_overrides"
id = Column(Integer, primary_key=True, index=True)
@@ -224,7 +316,6 @@ class CostOverride(Base):
# ──────────────────────────── 提交历史版本 ────────────────────────────
class SubmissionHistory(Base):
- """内容提交的修改历史"""
__tablename__ = "submission_history"
id = Column(Integer, primary_key=True, index=True)
@@ -236,3 +327,17 @@ class SubmissionHistory(Base):
created_at = Column(DateTime, server_default=func.now())
submission = relationship("Submission", back_populates="history")
+
+
+# ──────────────────────────── 固定开支 ────────────────────────────
+
+class OverheadCost(Base):
+ __tablename__ = "overhead_costs"
+
+ id = Column(Integer, primary_key=True, index=True)
+ cost_type = Column(SAEnum(OverheadCostType), nullable=False)
+ amount = Column(Float, nullable=False)
+ record_month = Column(String(7), nullable=False)
+ note = Column(Text, nullable=True)
+ recorded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
+ created_at = Column(DateTime, server_default=func.now())
diff --git a/backend/routers/auth.py b/backend/routers/auth.py
index 887fbee..45c6012 100644
--- a/backend/routers/auth.py
+++ b/backend/routers/auth.py
@@ -27,8 +27,13 @@ def get_me(current_user: User = Depends(get_current_user)):
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,
+ role_id=current_user.role_id,
+ role_name=current_user.role_name,
+ permissions=current_user.permissions,
monthly_salary=current_user.monthly_salary,
+ bonus=current_user.bonus or 0,
+ social_insurance=current_user.social_insurance or 0,
+ monthly_total_cost=current_user.monthly_total_cost,
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
index 86e2326..a9d4b8b 100644
--- a/backend/routers/costs.py
+++ b/backend/routers/costs.py
@@ -5,14 +5,15 @@ 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
+ User, AIToolCost, AIToolCostAllocation, OutsourceCost,
+ CostOverride, OverheadCost, SubscriptionPeriod, CostAllocationType,
+ OutsourceType, OverheadCostType
)
from schemas import (
AIToolCostCreate, AIToolCostOut, OutsourceCostCreate, OutsourceCostOut,
- CostOverrideCreate
+ CostOverrideCreate, OverheadCostCreate, OverheadCostOut
)
-from auth import get_current_user, require_role
+from auth import get_current_user, require_permission
router = APIRouter(prefix="/api/costs", tags=["成本管理"])
@@ -22,7 +23,7 @@ router = APIRouter(prefix="/api/costs", tags=["成本管理"])
@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))
+ current_user: User = Depends(require_permission("cost:view"))
):
costs = db.query(AIToolCost).order_by(AIToolCost.record_date.desc()).all()
return [
@@ -44,7 +45,7 @@ def list_ai_tool_costs(
def create_ai_tool_cost(
req: AIToolCostCreate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER))
+ current_user: User = Depends(require_permission("cost:create"))
):
cost = AIToolCost(
tool_name=req.tool_name,
@@ -84,7 +85,7 @@ def create_ai_tool_cost(
def delete_ai_tool_cost(
cost_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("cost:delete"))
):
cost = db.query(AIToolCost).filter(AIToolCost.id == cost_id).first()
if not cost:
@@ -101,7 +102,7 @@ def delete_ai_tool_cost(
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))
+ current_user: User = Depends(require_permission("cost:view"))
):
q = db.query(OutsourceCost)
if project_id:
@@ -123,7 +124,7 @@ def list_outsource_costs(
def create_outsource_cost(
req: OutsourceCostCreate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER))
+ current_user: User = Depends(require_permission("cost:create"))
):
cost = OutsourceCost(
project_id=req.project_id,
@@ -150,7 +151,7 @@ def create_outsource_cost(
def delete_outsource_cost(
cost_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("cost:delete"))
):
cost = db.query(OutsourceCost).filter(OutsourceCost.id == cost_id).first()
if not cost:
@@ -166,7 +167,7 @@ def delete_outsource_cost(
def create_cost_override(
req: CostOverrideCreate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR))
+ current_user: User = Depends(require_permission("cost:create"))
):
override = CostOverride(
user_id=req.user_id,
@@ -186,7 +187,7 @@ 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))
+ current_user: User = Depends(require_permission("cost:view"))
):
q = db.query(CostOverride)
if user_id:
@@ -203,3 +204,66 @@ def list_cost_overrides(
}
for r in records
]
+
+
+# ──────────────────── 固定开支(办公室租金、水电费) ────────────────────
+
+@router.get("/overhead", response_model=List[OverheadCostOut])
+def list_overhead_costs(
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("cost:view"))
+):
+ costs = db.query(OverheadCost).order_by(OverheadCost.record_month.desc()).all()
+ return [
+ OverheadCostOut(
+ id=c.id,
+ cost_type=c.cost_type.value if hasattr(c.cost_type, 'value') else c.cost_type,
+ amount=c.amount,
+ record_month=c.record_month,
+ note=c.note,
+ recorded_by=c.recorded_by,
+ created_at=c.created_at,
+ )
+ for c in costs
+ ]
+
+
+@router.post("/overhead", response_model=OverheadCostOut)
+def create_overhead_cost(
+ req: OverheadCostCreate,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("cost:create"))
+):
+ cost = OverheadCost(
+ cost_type=OverheadCostType(req.cost_type),
+ amount=req.amount,
+ record_month=req.record_month,
+ note=req.note,
+ recorded_by=current_user.id,
+ )
+ db.add(cost)
+ db.commit()
+ db.refresh(cost)
+ return OverheadCostOut(
+ id=cost.id,
+ cost_type=cost.cost_type.value,
+ amount=cost.amount,
+ record_month=cost.record_month,
+ note=cost.note,
+ recorded_by=cost.recorded_by,
+ created_at=cost.created_at,
+ )
+
+
+@router.delete("/overhead/{cost_id}")
+def delete_overhead_cost(
+ cost_id: int,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("cost:delete"))
+):
+ cost = db.query(OverheadCost).filter(OverheadCost.id == cost_id).first()
+ if not cost:
+ raise HTTPException(status_code=404, detail="记录不存在")
+ db.delete(cost)
+ db.commit()
+ return {"message": "已删除"}
diff --git a/backend/routers/dashboard.py b/backend/routers/dashboard.py
index ad0a6ff..69d4590 100644
--- a/backend/routers/dashboard.py
+++ b/backend/routers/dashboard.py
@@ -5,14 +5,15 @@ 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,
+ User, Project, Submission, AIToolCost,
ProjectStatus, ProjectType, WorkType
)
-from auth import get_current_user, require_role
+from auth import get_current_user, require_permission
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
+ calc_outsource_cost_for_project, calc_overhead_cost_for_project,
+ calc_team_efficiency
)
router = APIRouter(prefix="/api", tags=["仪表盘与结算"])
@@ -21,7 +22,7 @@ router = APIRouter(prefix="/api", tags=["仪表盘与结算"])
@router.get("/dashboard")
def get_dashboard(
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("dashboard:view"))
):
"""全局仪表盘数据"""
# 项目概览
@@ -133,15 +134,18 @@ def get_dashboard(
total_labor_all = 0.0
total_ai_all = 0.0
total_outsource_all = 0.0
+ total_overhead_all = 0.0
for p in active + completed:
total_labor_all += calc_labor_cost_for_project(p.id, db)
total_ai_all += calc_ai_tool_cost_for_project(p.id, db)
total_outsource_all += calc_outsource_cost_for_project(p.id, db)
+ total_overhead_all += calc_overhead_cost_for_project(p.id, db)
cost_breakdown = [
{"name": "人力成本", "value": round(total_labor_all, 0)},
{"name": "AI工具", "value": round(total_ai_all, 0)},
{"name": "外包", "value": round(total_outsource_all, 0)},
+ {"name": "固定开支", "value": round(total_overhead_all, 0)},
]
# ── 图表数据:各项目产出对比(进行中项目) ──
@@ -157,6 +161,43 @@ def get_dashboard(
"target": p.target_total_seconds,
})
+ # ── 盈利概览 ──
+ total_contract = 0.0
+ total_cost_completed = 0.0
+ for s in settled:
+ if s.get("contract_amount"):
+ total_contract += s["contract_amount"]
+ total_cost_completed += s.get("total_cost", 0)
+ total_profit = total_contract - total_cost_completed
+ profit_rate = round(total_profit / total_contract * 100, 1) if total_contract > 0 else 0
+
+ # 进行中项目的合同额和当前成本
+ in_progress_contract = 0.0
+ in_progress_cost = 0.0
+ for p in active:
+ if p.contract_amount:
+ in_progress_contract += p.contract_amount
+ in_progress_cost += calc_labor_cost_for_project(p.id, db) + calc_ai_tool_cost_for_project(p.id, db) + calc_outsource_cost_for_project(p.id, db) + calc_overhead_cost_for_project(p.id, db)
+
+ # 每个项目的盈亏(用于柱状图)
+ profit_by_project = []
+ for s in settled:
+ if s.get("contract_amount") and s["contract_amount"] > 0:
+ profit_by_project.append({
+ "name": s["project_name"],
+ "profit": round((s.get("contract_amount", 0) or 0) - s.get("total_cost", 0), 0),
+ })
+
+ profitability = {
+ "total_contract": round(total_contract, 0),
+ "total_cost": round(total_cost_completed, 0),
+ "total_profit": round(total_profit, 0),
+ "profit_rate": profit_rate,
+ "in_progress_contract": round(in_progress_contract, 0),
+ "in_progress_cost": round(in_progress_cost, 0),
+ "profit_by_project": profit_by_project,
+ }
+
return {
"active_projects": len(active),
"completed_projects": len(completed),
@@ -167,6 +208,7 @@ def get_dashboard(
"projects": project_summaries,
"waste_ranking": waste_ranking,
"settled_projects": settled,
+ "profitability": profitability,
# 图表数据
"daily_trend": daily_trend,
"cost_breakdown": cost_breakdown,
@@ -178,7 +220,7 @@ def get_dashboard(
def get_settlement(
project_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("settlement:view"))
):
"""项目结算报告"""
project = db.query(Project).filter(Project.id == project_id).first()
@@ -191,7 +233,7 @@ def get_settlement(
def get_efficiency(
project_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER))
+ current_user: User = Depends(require_permission("efficiency:view"))
):
"""项目团队效率数据"""
return calc_team_efficiency(project_id, db)
diff --git a/backend/routers/projects.py b/backend/routers/projects.py
index 92c7a6a..dfad0f7 100644
--- a/backend/routers/projects.py
+++ b/backend/routers/projects.py
@@ -5,11 +5,11 @@ 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,
+ User, Project, Submission, ProjectType,
ProjectStatus, PhaseGroup, WorkType
)
from schemas import ProjectCreate, ProjectUpdate, ProjectOut
-from auth import get_current_user, require_role
+from auth import get_current_user, require_permission
router = APIRouter(prefix="/api/projects", tags=["项目管理"])
@@ -76,7 +76,7 @@ def list_projects(
def create_project(
req: ProjectCreate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR))
+ current_user: User = Depends(require_permission("project:create"))
):
project = Project(
name=req.name,
@@ -111,7 +111,7 @@ def update_project(
project_id: int,
req: ProjectUpdate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR))
+ current_user: User = Depends(require_permission("project:edit"))
):
p = db.query(Project).filter(Project.id == project_id).first()
if not p:
@@ -141,11 +141,36 @@ def update_project(
return enrich_project(p, db)
+@router.delete("/{project_id}")
+def delete_project(
+ project_id: int,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("project:delete"))
+):
+ """删除项目及其关联数据"""
+ from models import OutsourceCost, AIToolCostAllocation, CostOverride, SubmissionHistory
+ p = db.query(Project).filter(Project.id == project_id).first()
+ if not p:
+ raise HTTPException(status_code=404, detail="项目不存在")
+
+ # 删除关联数据
+ subs = db.query(Submission).filter(Submission.project_id == project_id).all()
+ for s in subs:
+ db.query(SubmissionHistory).filter(SubmissionHistory.submission_id == s.id).delete()
+ db.query(Submission).filter(Submission.project_id == project_id).delete()
+ db.query(OutsourceCost).filter(OutsourceCost.project_id == project_id).delete()
+ db.query(AIToolCostAllocation).filter(AIToolCostAllocation.project_id == project_id).delete()
+ db.query(CostOverride).filter(CostOverride.project_id == project_id).delete()
+ db.delete(p)
+ db.commit()
+ return {"message": "项目已删除"}
+
+
@router.post("/{project_id}/complete")
def complete_project(
project_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("project:complete"))
):
"""Owner 手动确认项目完成"""
p = db.query(Project).filter(Project.id == project_id).first()
diff --git a/backend/routers/roles.py b/backend/routers/roles.py
new file mode 100644
index 0000000..12ead9a
--- /dev/null
+++ b/backend/routers/roles.py
@@ -0,0 +1,113 @@
+"""角色管理路由"""
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from typing import List
+from database import get_db
+from models import Role, User, ALL_PERMISSIONS, PERMISSION_KEYS
+from auth import get_current_user, require_permission
+
+router = APIRouter(prefix="/api/roles", tags=["角色管理"])
+
+
+@router.get("/permissions")
+def get_all_permissions(current_user: User = Depends(get_current_user)):
+ """获取系统全部权限定义(供前端勾选面板使用)"""
+ groups = {}
+ for key, label, group in ALL_PERMISSIONS:
+ if group not in groups:
+ groups[group] = []
+ groups[group].append({"key": key, "label": label})
+ return [{"group": g, "permissions": perms} for g, perms in groups.items()]
+
+
+@router.get("/")
+def list_roles(
+ db: Session = Depends(get_db),
+ current_user: User = Depends(get_current_user)
+):
+ roles = db.query(Role).order_by(Role.is_system.desc(), Role.id).all()
+ return [
+ {
+ "id": r.id,
+ "name": r.name,
+ "description": r.description,
+ "permissions": r.permissions or [],
+ "is_system": bool(r.is_system),
+ "user_count": db.query(User).filter(User.role_id == r.id).count(),
+ "created_at": r.created_at,
+ }
+ for r in roles
+ ]
+
+
+@router.post("/")
+def create_role(
+ req: dict,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("role:manage"))
+):
+ name = req.get("name", "").strip()
+ if not name:
+ raise HTTPException(status_code=400, detail="角色名称不能为空")
+ if db.query(Role).filter(Role.name == name).first():
+ raise HTTPException(status_code=400, detail="角色名称已存在")
+
+ perms = [p for p in req.get("permissions", []) if p in PERMISSION_KEYS]
+ role = Role(
+ name=name,
+ description=req.get("description", ""),
+ permissions=perms,
+ is_system=0,
+ )
+ db.add(role)
+ db.commit()
+ db.refresh(role)
+ return {"id": role.id, "name": role.name, "message": "角色已创建"}
+
+
+@router.put("/{role_id}")
+def update_role(
+ role_id: int,
+ req: dict,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("role:manage"))
+):
+ role = db.query(Role).filter(Role.id == role_id).first()
+ if not role:
+ raise HTTPException(status_code=404, detail="角色不存在")
+
+ name = req.get("name")
+ if name is not None:
+ name = name.strip()
+ existing = db.query(Role).filter(Role.name == name, Role.id != role_id).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="角色名称已存在")
+ role.name = name
+
+ if "description" in req:
+ role.description = req["description"]
+
+ if "permissions" in req:
+ role.permissions = [p for p in req["permissions"] if p in PERMISSION_KEYS]
+
+ db.commit()
+ return {"message": "角色已更新"}
+
+
+@router.delete("/{role_id}")
+def delete_role(
+ role_id: int,
+ db: Session = Depends(get_db),
+ current_user: User = Depends(require_permission("role:manage"))
+):
+ role = db.query(Role).filter(Role.id == role_id).first()
+ if not role:
+ raise HTTPException(status_code=404, detail="角色不存在")
+ if role.is_system:
+ raise HTTPException(status_code=400, detail="系统内置角色不可删除")
+ user_count = db.query(User).filter(User.role_id == role_id).count()
+ if user_count > 0:
+ raise HTTPException(status_code=400, detail=f"该角色下还有 {user_count} 个用户,请先转移用户再删除")
+ db.delete(role)
+ db.commit()
+ return {"message": "角色已删除"}
diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py
index 7d937da..6d78605 100644
--- a/backend/routers/submissions.py
+++ b/backend/routers/submissions.py
@@ -5,11 +5,11 @@ from typing import List, Optional
from datetime import date
from database import get_db
from models import (
- User, Submission, SubmissionHistory, Project, UserRole,
+ User, Submission, SubmissionHistory, Project,
PhaseGroup, WorkType, ContentType, SubmitTo
)
from schemas import SubmissionCreate, SubmissionUpdate, SubmissionOut
-from auth import get_current_user, require_role
+from auth import get_current_user, require_permission
router = APIRouter(prefix="/api/submissions", tags=["内容提交"])
@@ -44,8 +44,8 @@ def list_submissions(
current_user: User = Depends(get_current_user)
):
q = db.query(Submission)
- # 成员只能看自己的
- if current_user.role == UserRole.MEMBER:
+ # 没有 user:view 权限的只能看自己的
+ if not current_user.has_permission("user:view"):
q = q.filter(Submission.user_id == current_user.id)
elif user_id:
q = q.filter(Submission.user_id == user_id)
@@ -98,7 +98,7 @@ 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))
+ current_user: User = Depends(require_permission("submission:create"))
):
"""高权限修改提交记录(需填写原因)"""
sub = db.query(Submission).filter(Submission.id == submission_id).first()
@@ -174,7 +174,7 @@ def update_submission(
def get_submission_history(
submission_id: int,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER, UserRole.SUPERVISOR, UserRole.LEADER))
+ current_user: User = Depends(require_permission("submission:view"))
):
"""查看提交的修改历史"""
records = db.query(SubmissionHistory).filter(
diff --git a/backend/routers/users.py b/backend/routers/users.py
index 5435975..99ab23e 100644
--- a/backend/routers/users.py
+++ b/backend/routers/users.py
@@ -3,9 +3,9 @@ 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 models import User, Role, PhaseGroup
from schemas import UserCreate, UserUpdate, UserOut
-from auth import get_current_user, hash_password, require_role
+from auth import get_current_user, hash_password, require_permission
router = APIRouter(prefix="/api/users", tags=["用户管理"])
@@ -14,8 +14,12 @@ 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,
+ role_id=u.role_id, role_name=u.role_name, permissions=u.permissions,
+ monthly_salary=u.monthly_salary,
+ bonus=u.bonus or 0,
+ social_insurance=u.social_insurance or 0,
+ monthly_total_cost=u.monthly_total_cost,
+ daily_cost=u.daily_cost,
is_active=u.is_active, created_at=u.created_at,
)
@@ -23,7 +27,7 @@ def user_to_out(u: User) -> UserOut:
@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))
+ current_user: User = Depends(require_permission("user:view"))
):
users = db.query(User).order_by(User.created_at.desc()).all()
return [user_to_out(u) for u in users]
@@ -33,7 +37,7 @@ def list_users(
def create_user(
req: UserCreate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("user:manage"))
):
if db.query(User).filter(User.username == req.username).first():
raise HTTPException(status_code=400, detail="用户名已存在")
@@ -42,8 +46,10 @@ def create_user(
password_hash=hash_password(req.password),
name=req.name,
phase_group=PhaseGroup(req.phase_group),
- role=UserRole(req.role),
+ role_id=req.role_id,
monthly_salary=req.monthly_salary,
+ bonus=req.bonus,
+ social_insurance=req.social_insurance,
)
db.add(user)
db.commit()
@@ -56,7 +62,7 @@ def update_user(
user_id: int,
req: UserUpdate,
db: Session = Depends(get_db),
- current_user: User = Depends(require_role(UserRole.OWNER))
+ current_user: User = Depends(require_permission("user:manage"))
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
@@ -65,10 +71,14 @@ def update_user(
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.role_id is not None:
+ user.role_id = req.role_id
if req.monthly_salary is not None:
user.monthly_salary = req.monthly_salary
+ if req.bonus is not None:
+ user.bonus = req.bonus
+ if req.social_insurance is not None:
+ user.social_insurance = req.social_insurance
if req.is_active is not None:
user.is_active = req.is_active
db.commit()
diff --git a/backend/schemas.py b/backend/schemas.py
index 9fef907..c90c0c1 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -23,15 +23,19 @@ class UserCreate(BaseModel):
password: str
name: str
phase_group: str # 前期/制作/后期
- role: str = "成员"
+ role_id: int
monthly_salary: float = 0
+ bonus: float = 0
+ social_insurance: float = 0
class UserUpdate(BaseModel):
name: Optional[str] = None
phase_group: Optional[str] = None
- role: Optional[str] = None
+ role_id: Optional[int] = None
monthly_salary: Optional[float] = None
+ bonus: Optional[float] = None
+ social_insurance: Optional[float] = None
is_active: Optional[int] = None
@@ -40,8 +44,13 @@ class UserOut(BaseModel):
username: str
name: str
phase_group: str
- role: str
+ role_id: int
+ role_name: str
+ permissions: List[str] = []
monthly_salary: float
+ bonus: float
+ social_insurance: float
+ monthly_total_cost: float
daily_cost: float
is_active: int
created_at: Optional[datetime] = None
@@ -214,6 +223,28 @@ class CostOverrideCreate(BaseModel):
reason: Optional[str] = None
+# ──────────────────────────── 固定开支 ────────────────────────────
+
+class OverheadCostCreate(BaseModel):
+ cost_type: str # 办公室租金/水电费
+ amount: float
+ record_month: str # YYYY-MM
+ note: Optional[str] = None
+
+
+class OverheadCostOut(BaseModel):
+ id: int
+ cost_type: str
+ amount: float
+ record_month: str
+ note: Optional[str] = None
+ recorded_by: int
+ created_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
# ──────────────────────────── 仪表盘 ────────────────────────────
class DashboardSummary(BaseModel):
diff --git a/backend/seed.py b/backend/seed.py
index cd722ae..3032b77 100644
--- a/backend/seed.py
+++ b/backend/seed.py
@@ -14,22 +14,35 @@ def seed():
db.execute(table.delete())
db.commit()
+ # ── 初始化内置角色 ──
+ roles = {}
+ for role_name, role_def in BUILTIN_ROLES.items():
+ role = Role(
+ name=role_name,
+ description=role_def["description"],
+ permissions=role_def["permissions"],
+ is_system=1,
+ )
+ db.add(role)
+ roles[role_name] = role
+ db.flush()
+
# ── 用户 ──
users = [
User(username="admin", password_hash=hash_password("admin123"),
- name="老板", phase_group=PhaseGroup.PRODUCTION, role=UserRole.OWNER, monthly_salary=0),
+ name="老板", phase_group=PhaseGroup.PRODUCTION, role_id=roles["超级管理员"].id, monthly_salary=0),
User(username="zhangsan", password_hash=hash_password("123456"),
- name="张三", phase_group=PhaseGroup.PRE, role=UserRole.LEADER, monthly_salary=15000),
+ name="张三", phase_group=PhaseGroup.PRE, role_id=roles["组长"].id, monthly_salary=15000),
User(username="lisi", password_hash=hash_password("123456"),
- name="李四", phase_group=PhaseGroup.PRODUCTION, role=UserRole.LEADER, monthly_salary=18000),
+ name="李四", phase_group=PhaseGroup.PRODUCTION, role_id=roles["组长"].id, monthly_salary=18000),
User(username="wangwu", password_hash=hash_password("123456"),
- name="王五", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000),
+ name="王五", phase_group=PhaseGroup.PRODUCTION, role_id=roles["成员"].id, monthly_salary=12000),
User(username="zhaoliu", password_hash=hash_password("123456"),
- name="赵六", phase_group=PhaseGroup.PRODUCTION, role=UserRole.MEMBER, monthly_salary=12000),
+ name="赵六", phase_group=PhaseGroup.PRODUCTION, role_id=roles["成员"].id, monthly_salary=12000),
User(username="sunqi", password_hash=hash_password("123456"),
- name="孙七", phase_group=PhaseGroup.POST, role=UserRole.MEMBER, monthly_salary=13000),
+ name="孙七", phase_group=PhaseGroup.POST, role_id=roles["成员"].id, monthly_salary=13000),
User(username="producer", password_hash=hash_password("123456"),
- name="陈制片", phase_group=PhaseGroup.PRODUCTION, role=UserRole.SUPERVISOR, monthly_salary=20000),
+ name="陈制片", phase_group=PhaseGroup.PRODUCTION, role_id=roles["主管"].id, monthly_salary=20000),
]
db.add_all(users)
db.flush()
@@ -69,7 +82,6 @@ def seed():
base_date = date.today() - timedelta(days=14)
submissions = []
- # 张三(前期组)给项目 A 和 D 做前期
for i in range(5):
d = base_date + timedelta(days=i)
submissions.append(Submission(
@@ -89,10 +101,9 @@ def seed():
submit_date=d,
))
- # 李四(制作组组长)主要做项目 A
for i in range(10):
d = base_date + timedelta(days=i)
- secs = 45 + (i % 3) * 15 # 45-75秒
+ secs = 45 + (i % 3) * 15
wt = WorkType.TEST if i < 2 else WorkType.PRODUCTION
submissions.append(Submission(
user_id=lisi.id, project_id=proj_a.id,
@@ -103,10 +114,9 @@ def seed():
submit_date=d,
))
- # 王五(制作组)做项目 A 和 B
for i in range(8):
d = base_date + timedelta(days=i)
- secs = 30 + (i % 4) * 20 # 30-90秒
+ secs = 30 + (i % 4) * 20
submissions.append(Submission(
user_id=wangwu.id, project_id=proj_a.id,
project_phase=PhaseGroup.PRODUCTION, work_type=WorkType.PRODUCTION,
@@ -127,10 +137,9 @@ def seed():
submit_date=d,
))
- # 赵六(制作组)做项目 A
for i in range(10):
d = base_date + timedelta(days=i)
- secs = 50 + (i % 2) * 30 # 50-80秒
+ secs = 50 + (i % 2) * 30
wt = WorkType.TEST if i < 1 else WorkType.PRODUCTION
submissions.append(Submission(
user_id=zhaoliu.id, project_id=proj_a.id,
@@ -141,7 +150,6 @@ def seed():
submit_date=d,
))
- # 孙七(后期组)剪辑
for i in range(3):
d = base_date + timedelta(days=i + 10)
submissions.append(Submission(
@@ -151,7 +159,6 @@ def seed():
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,
@@ -163,7 +170,6 @@ def seed():
db.add_all(submissions)
- # ── AI 工具成本 ──
db.add(AIToolCost(
tool_name="Midjourney", subscription_period=SubscriptionPeriod.MONTHLY,
amount=200, allocation_type=CostAllocationType.TEAM,
@@ -176,7 +182,6 @@ def seed():
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,
@@ -185,6 +190,7 @@ def seed():
db.commit()
print("[OK] seed data generated")
+ print(f" - roles: {len(roles)}")
print(f" - users: {len(users)}")
print(f" - projects: 4")
print(f" - submissions: {len(submissions)}")
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 9d489fb..ee862f0 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -57,6 +57,7 @@ export const projectApi = {
create: (data) => api.post('/projects/', data),
update: (id, data) => api.put(`/projects/${id}`, data),
get: (id) => api.get(`/projects/${id}`),
+ delete: (id) => api.delete(`/projects/${id}`),
complete: (id) => api.post(`/projects/${id}/complete`),
settlement: (id) => api.get(`/projects/${id}/settlement`),
efficiency: (id) => api.get(`/projects/${id}/efficiency`),
@@ -80,6 +81,18 @@ export const costApi = {
deleteOutsource: (id) => api.delete(`/costs/outsource/${id}`),
createOverride: (data) => api.post('/costs/overrides', data),
listOverrides: (params) => api.get('/costs/overrides', { params }),
+ listOverhead: () => api.get('/costs/overhead'),
+ createOverhead: (data) => api.post('/costs/overhead', data),
+ deleteOverhead: (id) => api.delete(`/costs/overhead/${id}`),
+}
+
+// ── 角色 ──
+export const roleApi = {
+ list: () => api.get('/roles/'),
+ create: (data) => api.post('/roles/', data),
+ update: (id, data) => api.put(`/roles/${id}`, data),
+ delete: (id) => api.delete(`/roles/${id}`),
+ permissions: () => api.get('/roles/permissions'),
}
// ── 仪表盘 ──
diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue
index dc8692d..93d370a 100644
--- a/frontend/src/components/Layout.vue
+++ b/frontend/src/components/Layout.vue
@@ -19,7 +19,7 @@
:to="item.path"
class="nav-item"
:class="{ active: isActive(item.path) }"
- v-show="!item.role || hasRole(item.role)"
+ v-show="!item.perm || authStore.hasPermission(item.perm)"
>
| 成员 | ++ {{ d.slice(5) }} + | +
|---|---|
| + {{ user.name }} + | ++ {{ getCellData(user.id, d).totalSecs }}s + | +