diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 6efd5a4..8c990da 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -46,14 +46,37 @@ def _build_milestone_out(m) -> MilestoneOut: 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 + # 中期产出(动画制作)— 对标目标时长 + animation_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.total_seconds > 0, + Submission.project_phase == PhaseGroup.PRODUCTION, + ).scalar() or 0 + + # 后期产出:按内容类型分组 + post_rows = db.query( + Submission.content_type, + sa_func.sum(Submission.total_seconds).label("secs"), + ).filter( + Submission.project_id == p.id, + Submission.total_seconds > 0, + Submission.project_phase == PhaseGroup.POST, + ).group_by(Submission.content_type).all() + + post_secs = sum(row.secs for row in post_rows) + post_breakdown = [ + {"type": ct.value if hasattr(ct, 'value') else ct, "seconds": round(secs, 1)} + for ct, secs in post_rows + ] + target = p.target_total_seconds - progress = round(total_secs / target * 100, 1) if target > 0 else 0 + progress = round(animation_secs / target * 100, 1) if target > 0 else 0 # 集中损耗计算 waste_data = calc_waste_for_project(p.id, db) @@ -83,7 +106,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: }, "production": { "progress_percent": progress, - "submitted_seconds": round(total_secs, 1), + "submitted_seconds": round(animation_secs, 1), "target_seconds": target, "waste": waste_data.get("production_waste", {}), }, @@ -103,10 +126,9 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: else: current_stage = "已完成" - # EP 集数进度(批量查询,避免 N+1) + # EP 集数进度(批量查询,避免 N+1)— 只统计中期产出 episode_progress = [] ep_target = p.episode_duration_minutes * 60 # 每集目标秒数 - # 一次查出所有有提交的集数数据 ep_rows = db.query( Submission.episode_number, User.name, @@ -115,6 +137,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: Submission.project_id == p.id, Submission.episode_number.isnot(None), Submission.total_seconds > 0, + Submission.project_phase == PhaseGroup.PRODUCTION, ).group_by(Submission.episode_number, User.name).all() # 按集数聚合 @@ -154,6 +177,9 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: contract_amount=p.contract_amount, created_at=p.created_at, total_submitted_seconds=round(total_secs, 1), + animation_seconds=round(animation_secs, 1), + post_production_seconds=round(post_secs, 1), + post_production_breakdown=post_breakdown, progress_percent=progress, waste_seconds=round(waste_seconds, 1), waste_hours=waste_hours, diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py index fb8b00f..34548cf 100644 --- a/backend/routers/submissions.py +++ b/backend/routers/submissions.py @@ -95,8 +95,8 @@ def create_submission( episode_list = [None] # 项目级,单条无集数 elif req.episode_numbers and len(req.episode_numbers) > 0: episode_list = req.episode_numbers # 批量多集 - elif req.episode_number: - episode_list = [req.episode_number] # 单集 + elif req.episode_number is not None: + episode_list = [req.episode_number] # 单集(含0=全集通用) else: raise HTTPException(status_code=422, detail="请选择集数") diff --git a/backend/schemas.py b/backend/schemas.py index 7a20210..8278042 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -136,6 +136,9 @@ class ProjectOut(BaseModel): created_at: Optional[datetime] = None # 动态计算字段 total_submitted_seconds: Optional[float] = 0 + animation_seconds: Optional[float] = 0 # 中期动画产出 + post_production_seconds: Optional[float] = 0 # 后期产出合计 + post_production_breakdown: Optional[List[dict]] = [] # [{"type":"剪辑","seconds":300}, ...] progress_percent: Optional[float] = 0 waste_seconds: Optional[float] = 0 waste_hours: Optional[float] = 0 diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue index ba0ac78..934946c 100644 --- a/frontend/src/views/ProjectDetail.vue +++ b/frontend/src/views/ProjectDetail.vue @@ -53,8 +53,8 @@
{{ formatSecs(project.target_total_seconds) }}
-
已提交
-
{{ formatSecs(project.total_submitted_seconds) }}
+
制作产出
+
{{ formatSecs(project.animation_seconds) }}
当前阶段
@@ -67,6 +67,19 @@
+ +
+
+ + {{ formatSecs(project.post_production_seconds) }} +
+
+ + {{ item.type }} {{ formatSecs(item.seconds) }} + +
+
+
@@ -148,7 +161,7 @@
已产出 - {{ formatSecs(project.total_submitted_seconds) }} + {{ formatSecs(project.animation_seconds) }}
目标 @@ -911,6 +924,18 @@ onUnmounted(() => { .stat-label { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; } .stat-value { font-size: 22px; font-weight: 700; color: var(--text-primary); } +/* 后期产出全宽卡片 */ +.post-card { + display: flex; flex-direction: column; align-items: center; + background: var(--bg-card); border: 1px solid var(--border-color); + border-radius: var(--radius-md); padding: 16px 24px; margin-bottom: 16px; +} +.post-main { display: flex; align-items: baseline; gap: 12px; } +.post-label { font-size: 12px; color: var(--text-secondary); } +.post-value { font-size: 20px; font-weight: 700; color: var(--text-primary); } +.post-breakdown { display: flex; flex-wrap: wrap; gap: 4px 16px; justify-content: center; margin-top: 6px; } +.post-breakdown-item { font-size: 12px; color: var(--text-secondary); white-space: nowrap; } + .card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); margin-bottom: 16px; diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue index f50de11..52eb1d2 100644 --- a/frontend/src/views/Submissions.vue +++ b/frontend/src/views/Submissions.vue @@ -99,8 +99,9 @@ + -
+
已选 {{ form.episode_numbers.length }} 集,投入时长将平均分配到每集
@@ -186,6 +187,7 @@ + @@ -346,6 +348,20 @@ watch(() => form.content_type, (val) => { } }) +// "全集通用"(0) 与具体集数互斥 +watch(() => form.episode_numbers, (val, old) => { + if (!val || val.length < 2) return + const hadZero = old && old.includes(0) + const hasZero = val.includes(0) + if (hasZero && !hadZero) { + // 刚选了全集通用 → 只保留0 + form.episode_numbers = [0] + } else if (hasZero && hadZero && val.length > 1) { + // 有全集通用又选了具体集 → 去掉0 + form.episode_numbers = val.filter(v => v !== 0) + } +}) + watch(() => editForm.content_type, (val) => { if (NO_DURATION_TYPES.has(val)) { editForm.duration_minutes = 0