diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 8c990da..32f268c 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -7,7 +7,7 @@ from datetime import datetime from database import get_db from models import ( User, Project, Submission, ProjectType, - ProjectStatus, PhaseGroup, WorkType, + ProjectStatus, PhaseGroup, WorkType, ContentType, ProjectMilestone, DEFAULT_MILESTONES ) from schemas import ( @@ -59,6 +59,13 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: Submission.project_phase == PhaseGroup.PRODUCTION, ).scalar() or 0 + # 修补镜头(后期秒数,归入制作目标对比) + shot_repair_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == p.id, + Submission.content_type == ContentType.SHOT_REPAIR, + Submission.total_seconds > 0, + ).scalar() or 0 + # 后期产出:按内容类型分组 post_rows = db.query( Submission.content_type, @@ -107,6 +114,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut: "production": { "progress_percent": progress, "submitted_seconds": round(animation_secs, 1), + "shot_repair_seconds": round(shot_repair_secs, 1), "target_seconds": target, "waste": waste_data.get("production_waste", {}), }, diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py index 34548cf..74d24dc 100644 --- a/backend/routers/submissions.py +++ b/backend/routers/submissions.py @@ -88,6 +88,14 @@ def create_submission( if req.hours_spent is None or req.hours_spent <= 0: raise HTTPException(status_code=422, detail="请填写投入时长") + # 产出时长校验:前期内容不需要,中期/后期内容必须 > 0 + PRE_PHASE_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'} + content_val = req.content_type.value if hasattr(req.content_type, 'value') else req.content_type + if content_val not in PRE_PHASE_TYPES: + total_secs = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0) + if total_secs <= 0: + raise HTTPException(status_code=422, detail="请填写产出时长") + # 确定集数列表 from models import PROJECT_LEVEL_TYPES content_val = req.content_type.value if hasattr(req.content_type, 'value') else req.content_type @@ -211,6 +219,12 @@ def update_submission( # 重算总秒数 sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0) + # 产出时长校验:前期内容不需要,中期/后期内容必须 > 0 + PRE_PHASE_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'} + content_val = sub.content_type.value if hasattr(sub.content_type, 'value') else sub.content_type + if content_val not in PRE_PHASE_TYPES and sub.total_seconds <= 0: + raise HTTPException(status_code=422, detail="请填写产出时长") + # 保存新数据 new_data = { "project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase, diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue index 9587427..25b0559 100644 --- a/frontend/src/views/ProjectDetail.vue +++ b/frontend/src/views/ProjectDetail.vue @@ -167,6 +167,10 @@ 目标 {{ formatSecs(project.target_total_seconds) }} +
+ 修补镜头 + {{ formatSecs(shotRepairSeconds) }} +
测试损耗 {{ formatSecs(prodWaste.test_waste_seconds) }} @@ -605,6 +609,7 @@ const postMilestones = computed(() => (project.value.milestones || []).filter(m const preWasteHours = computed(() => project.value.phase_summary?.pre?.waste_hours || 0) const postWasteHours = computed(() => project.value.phase_summary?.post?.waste?.days_waste_hours || 0) const prodWaste = computed(() => project.value.phase_summary?.production?.waste || {}) +const shotRepairSeconds = computed(() => project.value.phase_summary?.production?.shot_repair_seconds || 0) const newMilestone = reactive({ pre: '', post: '' }) // ── 里程碑编辑 ── diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue index 52eb1d2..c4741c2 100644 --- a/frontend/src/views/Submissions.vue +++ b/frontend/src/views/Submissions.vue @@ -105,13 +105,12 @@ 已选 {{ form.episode_numbers.length }} 集,投入时长将平均分配到每集
- +
- 无产出秒数的工作可填 0
@@ -190,7 +189,7 @@ - +
@@ -434,6 +433,10 @@ async function handleCreate() { if (!form.project_id) { ElMessage.warning('请选择项目'); return } if (needsEpisode(form.content_type) && form.episode_numbers.length === 0) { ElMessage.warning('请选择集数'); return } if (!form.description?.trim()) { ElMessage.warning('请填写描述'); return } + if (showDuration(form.content_type)) { + const totalSecs = (form.duration_minutes || 0) * 60 + (form.duration_seconds || 0) + if (totalSecs <= 0) { ElMessage.warning('请填写产出时长'); return } + } if (!form.hours_spent || form.hours_spent <= 0) { ElMessage.warning('请填写投入时长'); return } if (isMilestoneOverdue.value && !form.delay_reason?.trim()) { ElMessage.warning('该里程碑已超期,请填写延期原因') @@ -491,6 +494,10 @@ async function handleUpdate() { ElMessage.warning('请填写修改原因') return } + if (showDuration(editForm.content_type)) { + const totalSecs = (editForm.duration_minutes || 0) * 60 + (editForm.duration_seconds || 0) + if (totalSecs <= 0) { ElMessage.warning('请填写产出时长'); return } + } editing.value = true try { const { _id, _project_name, _project_id, ...payload } = editForm