feat: 批量多集提交 — 集数多选+投入时长自动均分
- 后端 create_submission 支持 episode_numbers 批量创建,hours_spent 按集数均分 - 前端集数选择器改为多选(collapse-tags),显示分配提示 - 后端 API 端口统一改为 8001(.env.development + vite.config.js) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f07126e0ca
commit
41c2b9cd89
@ -66,7 +66,7 @@ def list_submissions(
|
||||
return [submission_to_out(s) for s in subs]
|
||||
|
||||
|
||||
@router.post("", response_model=SubmissionOut)
|
||||
@router.post("")
|
||||
def create_submission(
|
||||
req: SubmissionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
@ -88,12 +88,16 @@ def create_submission(
|
||||
if req.hours_spent is None or req.hours_spent <= 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
|
||||
if content_val in PROJECT_LEVEL_TYPES:
|
||||
req.episode_number = None
|
||||
elif not req.episode_number:
|
||||
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] # 单集
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail="请选择集数")
|
||||
|
||||
# 校验项目存在
|
||||
@ -101,10 +105,10 @@ def create_submission(
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 自动计算总秒数
|
||||
# 自动计算总秒数(每集)
|
||||
total_seconds = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0)
|
||||
|
||||
# 自动关联里程碑:根据 content_type 匹配同名里程碑
|
||||
# 自动关联里程碑
|
||||
milestone_id = None
|
||||
content_val = req.content_type
|
||||
if content_val in CONTENT_PHASE_MAP:
|
||||
@ -114,7 +118,6 @@ def create_submission(
|
||||
).first()
|
||||
if ms:
|
||||
milestone_id = ms.id
|
||||
# 超期校验:如果里程碑已超期,必须填写延期原因
|
||||
if ms.estimated_days and ms.start_date:
|
||||
expected_end = ms.start_date + timedelta(days=ms.estimated_days)
|
||||
if req.submit_date > expected_end and not req.delay_reason:
|
||||
@ -123,27 +126,38 @@ def create_submission(
|
||||
detail=f"里程碑「{ms.name}」已超期(预估{ms.estimated_days}天),请填写延期原因"
|
||||
)
|
||||
|
||||
sub = Submission(
|
||||
user_id=target_user_id,
|
||||
project_id=req.project_id,
|
||||
project_phase=PhaseGroup(req.project_phase),
|
||||
work_type=WorkType(req.work_type),
|
||||
content_type=ContentType(req.content_type),
|
||||
duration_minutes=req.duration_minutes or 0,
|
||||
duration_seconds=req.duration_seconds or 0,
|
||||
total_seconds=total_seconds,
|
||||
hours_spent=req.hours_spent,
|
||||
submit_to=SubmitTo(req.submit_to),
|
||||
description=req.description,
|
||||
submit_date=req.submit_date,
|
||||
milestone_id=milestone_id,
|
||||
delay_reason=req.delay_reason,
|
||||
episode_number=req.episode_number,
|
||||
)
|
||||
db.add(sub)
|
||||
# 投入时长平均分配到每集
|
||||
ep_count = len(episode_list)
|
||||
hours_per_ep = round(req.hours_spent / ep_count, 2)
|
||||
|
||||
results = []
|
||||
for ep in episode_list:
|
||||
sub = Submission(
|
||||
user_id=target_user_id,
|
||||
project_id=req.project_id,
|
||||
project_phase=PhaseGroup(req.project_phase),
|
||||
work_type=WorkType(req.work_type),
|
||||
content_type=ContentType(req.content_type),
|
||||
duration_minutes=req.duration_minutes or 0,
|
||||
duration_seconds=req.duration_seconds or 0,
|
||||
total_seconds=total_seconds,
|
||||
hours_spent=hours_per_ep,
|
||||
submit_to=SubmitTo(req.submit_to),
|
||||
description=req.description,
|
||||
submit_date=req.submit_date,
|
||||
milestone_id=milestone_id,
|
||||
delay_reason=req.delay_reason,
|
||||
episode_number=ep,
|
||||
)
|
||||
db.add(sub)
|
||||
results.append(sub)
|
||||
db.commit()
|
||||
db.refresh(sub)
|
||||
return submission_to_out(sub)
|
||||
for s in results:
|
||||
db.refresh(s)
|
||||
|
||||
if len(results) == 1:
|
||||
return submission_to_out(results[0])
|
||||
return [submission_to_out(s) for s in results]
|
||||
|
||||
|
||||
@router.put("/{submission_id}", response_model=SubmissionOut)
|
||||
|
||||
@ -166,6 +166,7 @@ class SubmissionCreate(BaseModel):
|
||||
submit_date: date
|
||||
delay_reason: Optional[str] = None
|
||||
episode_number: Optional[int] = None
|
||||
episode_numbers: Optional[List[int]] = None # 批量多集提交
|
||||
user_id: Optional[int] = None # 代人提交时指定目标用户
|
||||
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_API_BASE_URL=http://localhost:8001/api
|
||||
|
||||
@ -97,9 +97,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="集数" v-if="needsEpisode(form.content_type) && episodeOptions.length > 0" required>
|
||||
<el-select v-model="form.episode_number" placeholder="选择集数(必填)" style="width:100%">
|
||||
<el-select v-model="form.episode_numbers" multiple collapse-tags collapse-tags-tooltip placeholder="选择集数(可多选)" style="width:100%">
|
||||
<el-option v-for="ep in episodeOptions" :key="ep" :label="'EP' + String(ep).padStart(2,'0')" :value="ep" />
|
||||
</el-select>
|
||||
<div v-if="form.episode_numbers.length > 1" class="field-hint" style="margin-top:4px;color:#E6A23C">
|
||||
已选 {{ form.episode_numbers.length }} 集,投入时长将平均分配到每集
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="产出时长" v-if="showDuration(form.content_type)">
|
||||
<div class="inline-field">
|
||||
@ -283,7 +286,7 @@ const form = reactive({
|
||||
project_id: null, project_phase: '中期', work_type: '制作',
|
||||
content_type: '动画制作', duration_minutes: 0, duration_seconds: 0,
|
||||
hours_spent: null, submit_to: '组长', description: '', submit_date: today,
|
||||
delay_reason: '', episode_number: null, user_id: null,
|
||||
delay_reason: '', episode_numbers: [], user_id: null,
|
||||
})
|
||||
|
||||
// ── 编辑逻辑 ──
|
||||
@ -339,7 +342,7 @@ watch(() => form.content_type, (val) => {
|
||||
form.duration_seconds = 0
|
||||
}
|
||||
if (!needsEpisode(val)) {
|
||||
form.episode_number = null
|
||||
form.episode_numbers = []
|
||||
}
|
||||
})
|
||||
|
||||
@ -413,7 +416,7 @@ async function load() {
|
||||
|
||||
async function handleCreate() {
|
||||
if (!form.project_id) { ElMessage.warning('请选择项目'); return }
|
||||
if (needsEpisode(form.content_type) && !form.episode_number) { 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.hours_spent || form.hours_spent <= 0) { ElMessage.warning('请填写投入时长'); return }
|
||||
if (isMilestoneOverdue.value && !form.delay_reason?.trim()) {
|
||||
@ -431,7 +434,7 @@ async function handleCreate() {
|
||||
form.hours_spent = null
|
||||
form.description = ''
|
||||
form.delay_reason = ''
|
||||
form.episode_number = null
|
||||
form.episode_numbers = []
|
||||
form.user_id = null
|
||||
load()
|
||||
// 刷新工时进度
|
||||
|
||||
@ -7,7 +7,7 @@ export default defineConfig({
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user