diff --git a/backend/calculations.py b/backend/calculations.py
index d7c4dda..0ed9f35 100644
--- a/backend/calculations.py
+++ b/backend/calculations.py
@@ -346,20 +346,17 @@ def calc_waste_for_project(project_id: int, db: Session) -> dict:
def calc_team_efficiency(project_id: int, db: Session) -> list:
"""
- 人均基准对比法:
- - 人均基准 = 目标秒数 ÷ 参与制作人数
- - 每人超出比例 = (个人提交 - 人均基准) / 人均基准
+ 时均产出效率法(展示日均):
+ - 效率对比用时均产出 = 累计秒数 ÷ 累计工时(跨项目公平)
+ - 前端展示日均产出 = 累计秒数 ÷ 提交天数(直观)
"""
- project = db.query(Project).filter(Project.id == project_id).first()
- if not project:
- return []
+ from sqlalchemy import distinct
- target = project.target_total_seconds
-
- # 获取每个人的提交总秒数(仅有秒数的提交)
per_user = db.query(
Submission.user_id,
sa_func.sum(Submission.total_seconds).label("total_secs"),
+ sa_func.sum(Submission.hours_spent).label("total_hours"),
+ sa_func.count(distinct(Submission.submit_date)).label("days"),
sa_func.count(Submission.id).label("count"),
).filter(
Submission.project_id == project_id,
@@ -369,26 +366,32 @@ def calc_team_efficiency(project_id: int, db: Session) -> list:
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_data = []
+ for user_id, total_secs, total_hours, days, 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({
+ daily_avg = total_secs / days if days > 0 else 0
+ hourly_output = total_secs / total_hours if total_hours and total_hours > 0 else 0
+ user_data.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,
+ "total_hours": round(total_hours or 0, 1),
+ "active_days": days,
+ "daily_avg": round(daily_avg, 1),
+ "hourly_output": round(hourly_output, 1),
})
- result.sort(key=lambda x: x["total_seconds"], reverse=True)
- return result
+ # 效率对比用时均产出(公平)
+ team_hourly_avg = sum(d["hourly_output"] for d in user_data) / len(user_data)
+
+ for d in user_data:
+ diff = d["hourly_output"] - team_hourly_avg
+ d["team_hourly_avg"] = round(team_hourly_avg, 1)
+ d["efficiency_rate"] = round(diff / team_hourly_avg * 100, 1) if team_hourly_avg > 0 else 0
+
+ user_data.sort(key=lambda x: x["hourly_output"], reverse=True)
+ return user_data
# ──────────────────────────── 项目完整结算 ────────────────────────────
diff --git a/backend/main.py b/backend/main.py
index bfde9e7..18815ad 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -9,7 +9,7 @@ from fastapi.responses import FileResponse
from database import engine, Base
from models import (
User, Role, PhaseGroup, BUILTIN_ROLES, COST_PERM_MIGRATION,
- Project, ProjectMilestone, DEFAULT_MILESTONES
+ Project, ProjectMilestone, DEFAULT_MILESTONES, Submission
)
from auth import hash_password
from sqlalchemy.orm.attributes import flag_modified
@@ -134,6 +134,11 @@ def init_roles_and_admin():
conn.commit()
print("[MIGRATE] added milestone_id, delay_reason to submissions")
+ if 'episode_number' not in sub_cols:
+ conn.execute(text("ALTER TABLE submissions ADD COLUMN episode_number INT NULL"))
+ conn.commit()
+ print("[MIGRATE] added episode_number to submissions")
+
# MySQL: 扩展 content_type 枚举(使用 Python enum 名称)+ 旧值迁移
from config import DATABASE_URL
if not DATABASE_URL.startswith("sqlite"):
@@ -149,6 +154,17 @@ def init_roles_and_admin():
except Exception:
pass # 已经扩展过
+ # MySQL: 扩展 work_type 枚举(加入 REVISION/修改)
+ try:
+ conn.execute(text("""
+ ALTER TABLE submissions MODIFY COLUMN work_type
+ ENUM('PRODUCTION','TEST','PLAN','REVISION','QC') NOT NULL
+ """))
+ conn.commit()
+ print("[MIGRATE] expanded work_type enum")
+ except Exception:
+ pass # 已经扩展过
+
# 旧值迁移:DESIGN → PLANNING
r1 = conn.execute(text("UPDATE submissions SET content_type='ANIMATION' WHERE content_type='DESIGN'"))
conn.commit()
@@ -236,6 +252,25 @@ def init_roles_and_admin():
if orphans:
db.commit()
+ # 迁移:从 description 提取 EP 号填充 episode_number
+ try:
+ import re as _re
+ orphan_subs = db.query(Submission).filter(
+ Submission.episode_number.is_(None),
+ Submission.description.isnot(None),
+ ).all()
+ migrated_ep = 0
+ for s in orphan_subs:
+ m = _re.match(r'EP(\d+)', s.description or '', _re.IGNORECASE)
+ if m:
+ s.episode_number = int(m.group(1))
+ migrated_ep += 1
+ if migrated_ep:
+ db.commit()
+ print(f"[MIGRATE] extracted episode_number for {migrated_ep} submissions")
+ except Exception as e:
+ print(f"[MIGRATE] episode extraction error (non-fatal): {e}")
+
# 创建默认管理员(关联超级管理员角色)
admin_role = db.query(Role).filter(Role.name == "超级管理员").first()
if admin_role and not db.query(User).filter(User.username == "admin").first():
diff --git a/backend/models.py b/backend/models.py
index f0a8ee9..b2a8801 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -126,6 +126,8 @@ class WorkType(str, enum.Enum):
PRODUCTION = "制作"
TEST = "测试"
PLAN = "方案"
+ REVISION = "修改"
+ QC = "QC"
class ContentType(str, enum.Enum):
@@ -285,6 +287,7 @@ class Submission(Base):
submit_date = Column(Date, nullable=False)
milestone_id = Column(Integer, ForeignKey("project_milestones.id"), nullable=True)
delay_reason = Column(Text, nullable=True)
+ episode_number = Column(Integer, nullable=True) # EP集数: 1=EP01, 2=EP02...
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="submissions")
diff --git a/backend/routers/projects.py b/backend/routers/projects.py
index c2553b5..a29896d 100644
--- a/backend/routers/projects.py
+++ b/backend/routers/projects.py
@@ -103,6 +103,32 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
else:
current_stage = "已完成"
+ # EP 集数进度
+ episode_progress = []
+ ep_target = p.episode_duration_minutes * 60 # 每集目标秒数
+ for ep in range(1, p.episode_count + 1):
+ ep_subs = db.query(Submission).filter(
+ Submission.project_id == p.id,
+ Submission.episode_number == ep,
+ Submission.total_seconds > 0,
+ ).all()
+ ep_total = sum(s.total_seconds for s in ep_subs)
+ # 每集贡献者
+ contributors = {}
+ for s in ep_subs:
+ name = s.user.name if s.user else "未知"
+ contributors[name] = contributors.get(name, 0) + s.total_seconds
+ episode_progress.append({
+ "episode": ep,
+ "total_seconds": round(ep_total, 1),
+ "target_seconds": ep_target,
+ "progress_percent": round(ep_total / ep_target * 100, 1) if ep_target > 0 else 0,
+ "contributors": [
+ {"name": k, "seconds": round(v, 1)}
+ for k, v in sorted(contributors.items(), key=lambda x: -x[1])
+ ],
+ })
+
return ProjectOut(
id=p.id, name=p.name,
project_type=p.project_type.value if hasattr(p.project_type, 'value') else p.project_type,
@@ -124,6 +150,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
milestones=milestones_out,
phase_summary=phase_summary,
current_stage=current_stage,
+ episode_progress=episode_progress,
)
diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py
index 5897cb6..5a24387 100644
--- a/backend/routers/submissions.py
+++ b/backend/routers/submissions.py
@@ -33,6 +33,7 @@ def submission_to_out(s: Submission) -> SubmissionOut:
submit_date=s.submit_date,
milestone_name=s.milestone.name if s.milestone else None,
delay_reason=s.delay_reason,
+ episode_number=s.episode_number,
created_at=s.created_at,
)
@@ -113,6 +114,7 @@ def create_submission(
submit_date=req.submit_date,
milestone_id=milestone_id,
delay_reason=req.delay_reason,
+ episode_number=req.episode_number,
)
db.add(sub)
db.commit()
@@ -165,6 +167,8 @@ def update_submission(
sub.description = req.description
if req.submit_date is not None:
sub.submit_date = req.submit_date
+ if req.episode_number is not None:
+ sub.episode_number = req.episode_number
# 重算总秒数
sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0)
diff --git a/backend/schemas.py b/backend/schemas.py
index ea87f61..67105f4 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -144,6 +144,8 @@ class ProjectOut(BaseModel):
milestones: Optional[List[MilestoneOut]] = []
phase_summary: Optional[dict] = None
current_stage: Optional[str] = None
+ # EP 集数进度
+ episode_progress: Optional[List[dict]] = []
class Config:
from_attributes = True
@@ -163,6 +165,7 @@ class SubmissionCreate(BaseModel):
description: Optional[str] = None
submit_date: date
delay_reason: Optional[str] = None
+ episode_number: Optional[int] = None
class SubmissionUpdate(BaseModel):
@@ -175,6 +178,7 @@ class SubmissionUpdate(BaseModel):
submit_to: Optional[str] = None
description: Optional[str] = None
submit_date: Optional[date] = None
+ episode_number: Optional[int] = None
change_reason: str # 修改必须填原因
@@ -196,6 +200,7 @@ class SubmissionOut(BaseModel):
submit_date: date
milestone_name: Optional[str] = None
delay_reason: Optional[str] = None
+ episode_number: Optional[int] = None
created_at: Optional[datetime] = None
class Config:
diff --git a/frontend/src/views/MemberDetail.vue b/frontend/src/views/MemberDetail.vue
index fc75a49..3989153 100644
--- a/frontend/src/views/MemberDetail.vue
+++ b/frontend/src/views/MemberDetail.vue
@@ -93,7 +93,7 @@