feat: 新增「修改」「QC」工作类型 + EP集数追踪
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 5m44s
Build and Deploy Web / build-and-deploy (push) Successful in 3m24s

- 工作类型新增「修改」(返工)和「QC」(质量审核),支持组长多角色工作记录
- 提交表单支持按集数(EP)归类,项目详情展示各集进度
- 热力图增加修改(橙色)、QC(绿色)颜色区分和图例
- 效率算法优化:产出按小时计算,损耗统计更精准
- 数据库自动迁移:episode_number字段 + work_type ENUM扩展

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-02-25 23:18:47 +08:00
parent 32658fa608
commit 087d4e1a6b
10 changed files with 233 additions and 50 deletions

View File

@ -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
# ──────────────────────────── 项目完整结算 ────────────────────────────

View File

@ -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():

View File

@ -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")

View File

@ -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,
)

View File

@ -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)

View File

@ -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:

View File

@ -93,7 +93,7 @@
<el-table-column prop="submit_date" label="日期" width="110" />
<el-table-column label="工作类型" width="80">
<template #default="{row}">
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : ''" size="small">{{ row.work_type }}</el-tag>
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : row.work_type === '修改' ? 'danger' : row.work_type === 'QC' ? 'success' : ''" size="small">{{ row.work_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="content_type" label="内容类型" width="100" />

View File

@ -224,26 +224,54 @@
</div>
</div>
<!-- 集数进度 -->
<div v-if="epProgress.length && epProgress.some(e => e.total_seconds > 0)" class="card">
<div class="card-header">
<span class="card-title">集数进度</span>
<span class="card-count">{{ epProgress.filter(e => e.progress_percent >= 100).length }}/{{ epProgress.length }} 集完成</span>
</div>
<div class="card-body ep-progress-body">
<div v-for="ep in epProgress" :key="ep.episode" class="ep-row">
<div class="ep-label">EP{{ String(ep.episode).padStart(2, '0') }}</div>
<div class="ep-bar-wrapper">
<div class="ep-bar-track">
<div v-for="(c, ci) in ep.contributors" :key="ci"
class="ep-bar-segment"
:style="{width: (c.seconds / ep.target_seconds * 100) + '%', background: memberColor(c.name)}"
:title="c.name + ' ' + formatSecs(c.seconds)">
</div>
</div>
<div class="ep-bar-info">
<span class="ep-percent" :class="{done: ep.progress_percent >= 100}">{{ ep.progress_percent }}%</span>
<span class="ep-total">{{ formatSecs(ep.total_seconds) }}</span>
</div>
</div>
<div class="ep-contributors" v-if="ep.contributors.length">
<span v-for="(c, ci) in ep.contributors" :key="ci" class="ep-contributor">
<span class="ep-contributor-dot" :style="{background: memberColor(c.name)}"></span>
{{ c.name }} {{ formatSecs(c.seconds) }}
</span>
</div>
</div>
</div>
</div>
<!-- 团队效率 -->
<div v-if="authStore.hasPermission('efficiency:view') && efficiency.length" class="card">
<div class="card-header"><span class="card-title">团队效率</span></div>
<div class="card-body">
<el-table :data="efficiency" size="small">
<el-table-column prop="user_name" label="成员" width="100" />
<el-table-column label="提交总量" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
<el-table-column label="人均基准" align="right"><template #default="{row}">{{ formatSecs(row.baseline) }}</template></el-table-column>
<el-table-column label="超出基准" align="right">
<el-table-column label="累计产出" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
<el-table-column label="工时" align="right" width="70"><template #default="{row}">{{ row.total_hours }}h</template></el-table-column>
<el-table-column label="日均产出" align="right"><template #default="{row}"><strong>{{ formatSecs(row.daily_avg) }}</strong></template></el-table-column>
<el-table-column label="效率" align="right" width="100">
<template #default="{row}">
<span :style="{color: row.excess_seconds > 0 ? '#FF3B30' : '#34C759', fontWeight: 600}">
{{ row.excess_seconds > 0 ? '+' : '' }}{{ formatSecs(row.excess_seconds) }}
<span class="rate-badge" :class="{success: row.efficiency_rate >= 0, danger: row.efficiency_rate < 0}">
{{ row.efficiency_rate > 0 ? '+' : '' }}{{ row.efficiency_rate }}%
</span>
</template>
</el-table-column>
<el-table-column label="超出比例" align="right" width="100">
<template #default="{row}">
<span class="rate-badge" :class="{danger: row.excess_rate > 20}">{{ row.excess_rate > 0 ? '+' : '' }}{{ row.excess_rate }}%</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
@ -288,8 +316,10 @@
<!-- 图例 -->
<div class="heatmap-legend">
<span class="legend-item"><span class="legend-dot type-制作"></span>制作</span>
<span class="legend-item"><span class="legend-dot type-修改"></span>修改</span>
<span class="legend-item"><span class="legend-dot type-测试"></span>测试</span>
<span class="legend-item"><span class="legend-dot type-方案"></span>方案</span>
<span class="legend-item"><span class="legend-dot type-QC"></span>QC</span>
</div>
</div>
<div class="card-body" v-else>
@ -300,7 +330,7 @@
<!-- 悬浮提示 -->
<div class="cell-tooltip" v-if="tooltip.visible" :style="{ top: tooltip.y + 'px', left: tooltip.x + 'px' }">
<div v-for="(s, i) in tooltip.items" :key="i" class="tooltip-row">
<el-tag :type="s.work_type === '测试' ? 'warning' : s.work_type === '方案' ? 'info' : ''" size="small">{{ s.work_type }}</el-tag>
<el-tag :type="s.work_type === '测试' ? 'warning' : s.work_type === '方案' ? 'info' : s.work_type === '修改' ? 'danger' : s.work_type === 'QC' ? 'success' : ''" size="small">{{ s.work_type }}</el-tag>
<span class="tooltip-type">{{ s.content_type }}</span>
<span class="tooltip-secs" v-if="s.total_seconds > 0">{{ formatSecs(s.total_seconds) }}</span>
<span class="tooltip-desc" v-if="s.description">{{ s.description }}</span>
@ -319,7 +349,7 @@
<div class="drawer-date-header">{{ date }}</div>
<div v-for="s in group" :key="s.id" class="drawer-sub-item">
<div class="drawer-sub-top">
<el-tag :type="s.work_type === '测试' ? 'warning' : s.work_type === '方案' ? 'info' : ''" size="small">{{ s.work_type }}</el-tag>
<el-tag :type="s.work_type === '测试' ? 'warning' : s.work_type === '方案' ? 'info' : s.work_type === '修改' ? 'danger' : s.work_type === 'QC' ? 'success' : ''" size="small">{{ s.work_type }}</el-tag>
<span>{{ s.content_type }}</span>
<span v-if="s.total_seconds > 0" class="drawer-sub-secs">{{ formatSecs(s.total_seconds) }}</span>
</div>
@ -421,7 +451,7 @@
<el-table-column prop="project_phase" label="阶段" width="70" />
<el-table-column label="工作类型" width="80">
<template #default="{row}">
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : ''" size="small">{{ row.work_type }}</el-tag>
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : row.work_type === '修改' ? 'danger' : row.work_type === 'QC' ? 'success' : ''" size="small">{{ row.work_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="content_type" label="内容类型" width="90" />
@ -714,6 +744,20 @@ function openMemberDrawer(user) {
drawerVisible.value = true
}
// EP
const epProgress = computed(() => project.value.episode_progress || [])
const EP_COLORS = ['#3370FF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#5AC8FA', '#FF2D55', '#FFCC00', '#8F959E', '#007AFF', '#30D158', '#FF6482', '#BF5AF2', '#64D2FF']
const memberColorMap = {}
let colorIdx = 0
function memberColor(name) {
if (!memberColorMap[name]) {
memberColorMap[name] = EP_COLORS[colorIdx % EP_COLORS.length]
colorIdx++
}
return memberColorMap[name]
}
//
function formatSecs(s) {
if (!s) return '0秒'
@ -860,6 +904,7 @@ onUnmounted(() => {
background: var(--bg-hover); padding: 2px 8px; border-radius: 4px; display: inline-block;
}
.rate-badge.danger { background: #FFE8E7; color: #FF3B30; }
.rate-badge.success { background: #E8F8EE; color: #34C759; }
.inline-field { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.field-unit { font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
.field-hint { font-size: 12px; color: var(--text-placeholder, #C0C4CC); margin-left: 4px; }
@ -986,8 +1031,10 @@ onUnmounted(() => {
.heatmap-cell:hover { filter: brightness(0.92); }
.heatmap-cell.has-data { font-weight: 600; color: #fff; }
.heatmap-cell.type-制作 { background: #3370FF; }
.heatmap-cell.type-修改 { background: #FF3B30; }
.heatmap-cell.type-测试 { background: #FF9500; }
.heatmap-cell.type-方案 { background: #8F959E; }
.heatmap-cell.type-QC { background: #34C759; }
.member-link {
color: #3370FF; cursor: pointer; text-decoration: none;
}
@ -997,8 +1044,39 @@ onUnmounted(() => {
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.legend-dot.type-制作 { background: #3370FF; }
.legend-dot.type-修改 { background: #FF3B30; }
.legend-dot.type-测试 { background: #FF9500; }
.legend-dot.type-方案 { background: #8F959E; }
.legend-dot.type-QC { background: #34C759; }
/* EP 集数进度 */
.ep-progress-body { padding: 16px 20px !important; }
.ep-row { margin-bottom: 16px; }
.ep-row:last-child { margin-bottom: 0; }
.ep-label {
font-size: 13px; font-weight: 600; color: var(--text-primary);
margin-bottom: 6px;
}
.ep-bar-wrapper { display: flex; align-items: center; gap: 12px; }
.ep-bar-track {
flex: 1; height: 16px; background: #E5E6EB; border-radius: 8px;
overflow: hidden; display: flex;
}
.ep-bar-segment {
height: 100%; min-width: 2px; transition: width 0.3s;
}
.ep-bar-segment:first-child { border-radius: 8px 0 0 8px; }
.ep-bar-segment:last-child { border-radius: 0 8px 8px 0; }
.ep-bar-segment:only-child { border-radius: 8px; }
.ep-bar-info { display: flex; gap: 8px; align-items: baseline; min-width: 120px; }
.ep-percent { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.ep-percent.done { color: #34C759; }
.ep-total { font-size: 12px; color: var(--text-placeholder, #C0C4CC); }
.ep-contributors {
display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; padding-left: 2px;
}
.ep-contributor { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-secondary); }
.ep-contributor-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
/* 悬浮提示 */
.cell-tooltip {

View File

@ -109,20 +109,16 @@
<template #header><span class="section-title">团队效率排行</span></template>
<el-table :data="data.team_efficiency" size="small" stripe>
<el-table-column prop="user_name" label="成员" width="100" />
<el-table-column label="提交总量" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
<el-table-column label="人均基准" align="right"><template #default="{row}">{{ formatSecs(row.baseline) }}</template></el-table-column>
<el-table-column label="超出" align="right">
<el-table-column label="累计产出" align="right"><template #default="{row}">{{ formatSecs(row.total_seconds) }}</template></el-table-column>
<el-table-column label="工时" align="right" width="70"><template #default="{row}">{{ row.total_hours }}h</template></el-table-column>
<el-table-column label="日均产出" align="right"><template #default="{row}"><strong>{{ formatSecs(row.daily_avg) }}</strong></template></el-table-column>
<el-table-column label="效率" align="right" width="100">
<template #default="{row}">
<span :style="{color: row.excess_seconds > 0 ? '#f56c6c' : '#67c23a'}">
{{ row.excess_seconds > 0 ? '+' : '' }}{{ formatSecs(row.excess_seconds) }}
<span :style="{color: row.efficiency_rate >= 0 ? '#34C759' : '#FF3B30', fontWeight: 600}">
{{ row.efficiency_rate > 0 ? '+' : '' }}{{ row.efficiency_rate }}%
</span>
</template>
</el-table-column>
<el-table-column label="超出比例" align="right" width="100">
<template #default="{row}">
<span :style="{color: row.excess_rate > 20 ? '#f56c6c' : '#333'}">{{ row.excess_rate > 0 ? '+' : '' }}{{ row.excess_rate }}%</span>
</template>
</el-table-column>
<el-table-column prop="submission_count" label="提交次数" width="90" align="right" />
</el-table>
</el-card>

View File

@ -24,7 +24,7 @@
<el-table-column prop="project_phase" label="阶段" width="70" />
<el-table-column label="工作类型" width="80">
<template #default="{row}">
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : ''" size="small">{{ row.work_type }}</el-tag>
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : row.work_type === '修改' ? 'danger' : row.work_type === 'QC' ? 'success' : ''" size="small">{{ row.work_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="content_type" label="内容类型" width="90" />
@ -64,12 +64,19 @@
<el-form-item label="工作类型" required>
<el-select v-model="form.work_type" style="width:100%">
<el-option label="制作" value="制作" />
<el-option label="修改" value="修改" />
<el-option label="测试" value="测试" />
<el-option label="方案" value="方案" />
<el-option label="QC" value="QC" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="集数" v-if="episodeOptions.length > 0">
<el-select v-model="form.episode_number" placeholder="选择集数(选填)" clearable style="width:100%">
<el-option v-for="ep in episodeOptions" :key="ep" :label="'EP' + String(ep).padStart(2,'0')" :value="ep" />
</el-select>
</el-form-item>
<el-form-item label="内容类型" required>
<el-select v-model="form.content_type" placeholder="本次提交的产出类型" style="width:100%">
<el-option-group label="前期">
@ -156,12 +163,19 @@
<el-form-item label="工作类型" required>
<el-select v-model="editForm.work_type" style="width:100%">
<el-option label="制作" value="制作" />
<el-option label="修改" value="修改" />
<el-option label="测试" value="测试" />
<el-option label="方案" value="方案" />
<el-option label="QC" value="QC" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="集数" v-if="editEpisodeOptions.length > 0">
<el-select v-model="editForm.episode_number" placeholder="选择集数(选填)" clearable style="width:100%">
<el-option v-for="ep in editEpisodeOptions" :key="ep" :label="'EP' + String(ep).padStart(2,'0')" :value="ep" />
</el-select>
</el-form-item>
<el-form-item label="内容类型" required>
<el-select v-model="editForm.content_type" style="width:100%">
<el-option-group label="前期">
@ -254,7 +268,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: '',
delay_reason: '', episode_number: null,
})
//
@ -279,6 +293,13 @@ const isMilestoneOverdue = computed(() => {
return ms?.is_overdue === true
})
// EP
const episodeOptions = computed(() => {
const proj = activeProjects.value.find(p => p.id === form.project_id)
if (!proj || !proj.episode_count) return []
return Array.from({length: proj.episode_count}, (_, i) => i + 1)
})
function formatSecs(s) {
if (!s) return '0秒'
const m = Math.floor(s / 60)
@ -314,6 +335,7 @@ async function handleCreate() {
form.hours_spent = null
form.description = ''
form.delay_reason = ''
form.episode_number = null
load()
} finally { creating.value = false }
}
@ -324,6 +346,7 @@ const editing = ref(false)
const editForm = reactive({
_id: null,
_project_name: '',
_project_id: null,
project_phase: '',
work_type: '',
content_type: '',
@ -334,11 +357,19 @@ const editForm = reactive({
description: '',
submit_date: '',
change_reason: '',
episode_number: null,
})
const editEpisodeOptions = computed(() => {
const proj = projects.value.find(p => p.id === editForm._project_id)
if (!proj || !proj.episode_count) return []
return Array.from({length: proj.episode_count}, (_, i) => i + 1)
})
function openEdit(row) {
editForm._id = row.id
editForm._project_name = row.project_name
editForm._project_id = row.project_id
editForm.project_phase = row.project_phase
editForm.work_type = row.work_type
editForm.content_type = row.content_type
@ -348,6 +379,7 @@ function openEdit(row) {
editForm.submit_to = row.submit_to
editForm.description = row.description || ''
editForm.submit_date = row.submit_date
editForm.episode_number = row.episode_number
editForm.change_reason = ''
showEdit.value = true
}
@ -359,7 +391,7 @@ async function handleUpdate() {
}
editing.value = true
try {
const { _id, _project_name, ...payload } = editForm
const { _id, _project_name, _project_id, ...payload } = editForm
await submissionApi.update(_id, payload)
ElMessage.success('修改成功')
showEdit.value = false