feat: 项目详情显示修补镜头 + 产出时长必填验证
- 项目详情中期模块显示修补镜头(后期秒数,归入制作产出计算) - 产出时长改为必填项(非前期内容必须 > 0) - 前端+后端双重验证,防止提交0秒产出 - 删除"无产出秒数的工作可填 0"过时提示 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe6136555b
commit
f9016ab2af
@ -7,7 +7,7 @@ from datetime import datetime
|
|||||||
from database import get_db
|
from database import get_db
|
||||||
from models import (
|
from models import (
|
||||||
User, Project, Submission, ProjectType,
|
User, Project, Submission, ProjectType,
|
||||||
ProjectStatus, PhaseGroup, WorkType,
|
ProjectStatus, PhaseGroup, WorkType, ContentType,
|
||||||
ProjectMilestone, DEFAULT_MILESTONES
|
ProjectMilestone, DEFAULT_MILESTONES
|
||||||
)
|
)
|
||||||
from schemas import (
|
from schemas import (
|
||||||
@ -59,6 +59,13 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
|
|||||||
Submission.project_phase == PhaseGroup.PRODUCTION,
|
Submission.project_phase == PhaseGroup.PRODUCTION,
|
||||||
).scalar() or 0
|
).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(
|
post_rows = db.query(
|
||||||
Submission.content_type,
|
Submission.content_type,
|
||||||
@ -107,6 +114,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
|
|||||||
"production": {
|
"production": {
|
||||||
"progress_percent": progress,
|
"progress_percent": progress,
|
||||||
"submitted_seconds": round(animation_secs, 1),
|
"submitted_seconds": round(animation_secs, 1),
|
||||||
|
"shot_repair_seconds": round(shot_repair_secs, 1),
|
||||||
"target_seconds": target,
|
"target_seconds": target,
|
||||||
"waste": waste_data.get("production_waste", {}),
|
"waste": waste_data.get("production_waste", {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -88,6 +88,14 @@ def create_submission(
|
|||||||
if req.hours_spent is None or req.hours_spent <= 0:
|
if req.hours_spent is None or req.hours_spent <= 0:
|
||||||
raise HTTPException(status_code=422, detail="请填写投入时长")
|
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
|
from models import PROJECT_LEVEL_TYPES
|
||||||
content_val = req.content_type.value if hasattr(req.content_type, 'value') else req.content_type
|
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)
|
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 = {
|
new_data = {
|
||||||
"project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase,
|
"project_phase": sub.project_phase.value if hasattr(sub.project_phase, 'value') else sub.project_phase,
|
||||||
|
|||||||
@ -167,6 +167,10 @@
|
|||||||
<span class="prod-info-label">目标</span>
|
<span class="prod-info-label">目标</span>
|
||||||
<span class="prod-info-value">{{ formatSecs(project.target_total_seconds) }}</span>
|
<span class="prod-info-value">{{ formatSecs(project.target_total_seconds) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="prod-info-row" v-if="shotRepairSeconds > 0">
|
||||||
|
<span class="prod-info-label">修补镜头</span>
|
||||||
|
<span class="prod-info-value" :style="{color: '#34C759'}">{{ formatSecs(shotRepairSeconds) }}</span>
|
||||||
|
</div>
|
||||||
<div class="prod-info-row">
|
<div class="prod-info-row">
|
||||||
<span class="prod-info-label">测试损耗</span>
|
<span class="prod-info-label">测试损耗</span>
|
||||||
<span class="prod-info-value" :style="{color: prodWaste.test_waste_seconds > 0 ? '#FF9500' : 'inherit'}">{{ formatSecs(prodWaste.test_waste_seconds) }}</span>
|
<span class="prod-info-value" :style="{color: prodWaste.test_waste_seconds > 0 ? '#FF9500' : 'inherit'}">{{ formatSecs(prodWaste.test_waste_seconds) }}</span>
|
||||||
@ -605,6 +609,7 @@ const postMilestones = computed(() => (project.value.milestones || []).filter(m
|
|||||||
const preWasteHours = computed(() => project.value.phase_summary?.pre?.waste_hours || 0)
|
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 postWasteHours = computed(() => project.value.phase_summary?.post?.waste?.days_waste_hours || 0)
|
||||||
const prodWaste = computed(() => project.value.phase_summary?.production?.waste || {})
|
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: '' })
|
const newMilestone = reactive({ pre: '', post: '' })
|
||||||
|
|
||||||
// ── 里程碑编辑 ──
|
// ── 里程碑编辑 ──
|
||||||
|
|||||||
@ -105,13 +105,12 @@
|
|||||||
已选 {{ form.episode_numbers.length }} 集,投入时长将平均分配到每集
|
已选 {{ form.episode_numbers.length }} 集,投入时长将平均分配到每集
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="产出时长" v-if="showDuration(form.content_type)">
|
<el-form-item label="产出时长" v-if="showDuration(form.content_type)" required>
|
||||||
<div class="inline-field">
|
<div class="inline-field">
|
||||||
<el-input-number v-model="form.duration_minutes" :min="0" :step="1" style="width:120px" />
|
<el-input-number v-model="form.duration_minutes" :min="0" :step="1" style="width:120px" />
|
||||||
<span class="field-unit">分</span>
|
<span class="field-unit">分</span>
|
||||||
<el-input-number v-model="form.duration_seconds" :min="0" :max="59" :step="5" style="width:120px" />
|
<el-input-number v-model="form.duration_seconds" :min="0" :max="59" :step="5" style="width:120px" />
|
||||||
<span class="field-unit">秒</span>
|
<span class="field-unit">秒</span>
|
||||||
<span class="field-hint">无产出秒数的工作可填 0</span>
|
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="投入时长" required>
|
<el-form-item label="投入时长" required>
|
||||||
@ -190,7 +189,7 @@
|
|||||||
<el-option :key="0" label="全集通用" :value="0" />
|
<el-option :key="0" label="全集通用" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="产出时长" v-if="showDuration(editForm.content_type)">
|
<el-form-item label="产出时长" v-if="showDuration(editForm.content_type)" required>
|
||||||
<div class="inline-field">
|
<div class="inline-field">
|
||||||
<el-input-number v-model="editForm.duration_minutes" :min="0" :step="1" style="width:120px" />
|
<el-input-number v-model="editForm.duration_minutes" :min="0" :step="1" style="width:120px" />
|
||||||
<span class="field-unit">分</span>
|
<span class="field-unit">分</span>
|
||||||
@ -434,6 +433,10 @@ async function handleCreate() {
|
|||||||
if (!form.project_id) { ElMessage.warning('请选择项目'); return }
|
if (!form.project_id) { ElMessage.warning('请选择项目'); return }
|
||||||
if (needsEpisode(form.content_type) && form.episode_numbers.length === 0) { ElMessage.warning('请选择集数'); return }
|
if (needsEpisode(form.content_type) && form.episode_numbers.length === 0) { ElMessage.warning('请选择集数'); return }
|
||||||
if (!form.description?.trim()) { 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 (!form.hours_spent || form.hours_spent <= 0) { ElMessage.warning('请填写投入时长'); return }
|
||||||
if (isMilestoneOverdue.value && !form.delay_reason?.trim()) {
|
if (isMilestoneOverdue.value && !form.delay_reason?.trim()) {
|
||||||
ElMessage.warning('该里程碑已超期,请填写延期原因')
|
ElMessage.warning('该里程碑已超期,请填写延期原因')
|
||||||
@ -491,6 +494,10 @@ async function handleUpdate() {
|
|||||||
ElMessage.warning('请填写修改原因')
|
ElMessage.warning('请填写修改原因')
|
||||||
return
|
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
|
editing.value = true
|
||||||
try {
|
try {
|
||||||
const { _id, _project_name, _project_id, ...payload } = editForm
|
const { _id, _project_name, _project_id, ...payload } = editForm
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user