feat: 内部事务提交功能(培训/招聘面试/内部其他)
- 新增 ContentType: TRAINING, RECRUITMENT, INTERNAL_OTHER - 新增 PhaseGroup: INTERNAL (内部事务) - 前端选"内部事务"项目时隐藏项目阶段/工作类型/产出时长 - 内容类型改显示为"事务类型"(培训/招聘面试/内部其他) - 报告和仪表盘中排除内部事务项目 - 内部事务成本后续按管理成本逻辑分摊到所有项目 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bc9fa5a798
commit
becfd74efd
@ -161,13 +161,24 @@ def init_roles_and_admin():
|
||||
'SCRIPT','STORYBOARD','CHARACTER_DESIGN','SCENE_DESIGN',
|
||||
'PROP_DESIGN','ANIMATION','DUBBING','AI_DUBBING','SOUND_EFFECTS',
|
||||
'SHOT_REPAIR','EDITING','MUSIC','SUBTITLE','OTHER',
|
||||
'DESIGN') NOT NULL
|
||||
'DESIGN','TRAINING','RECRUITMENT','INTERNAL_OTHER') NOT NULL
|
||||
"""))
|
||||
conn.commit()
|
||||
print("[MIGRATE] expanded content_type enum")
|
||||
except Exception:
|
||||
pass # 已经扩展过
|
||||
|
||||
# MySQL: 扩展 project_phase 枚举(加入 INTERNAL/内部事务)
|
||||
try:
|
||||
conn.execute(text("""
|
||||
ALTER TABLE submissions MODIFY COLUMN project_phase
|
||||
ENUM('PRE','PRODUCTION','POST','INTERNAL') NOT NULL
|
||||
"""))
|
||||
conn.commit()
|
||||
print("[MIGRATE] expanded project_phase enum")
|
||||
except Exception:
|
||||
pass # 已经扩展过
|
||||
|
||||
# MySQL: 扩展 work_type 枚举(加入 REVISION/修改)
|
||||
try:
|
||||
conn.execute(text("""
|
||||
|
||||
@ -126,6 +126,7 @@ class PhaseGroup(str, enum.Enum):
|
||||
PRE = "前期"
|
||||
PRODUCTION = "中期"
|
||||
POST = "后期"
|
||||
INTERNAL = "内部事务"
|
||||
|
||||
|
||||
class WorkType(str, enum.Enum):
|
||||
@ -159,6 +160,10 @@ class ContentType(str, enum.Enum):
|
||||
SUBTITLE = "字幕"
|
||||
# 通用
|
||||
OTHER = "其他"
|
||||
# 内部事务
|
||||
TRAINING = "培训"
|
||||
RECRUITMENT = "招聘面试"
|
||||
INTERNAL_OTHER = "内部其他"
|
||||
|
||||
|
||||
class SubmitTo(str, enum.Enum):
|
||||
@ -449,7 +454,8 @@ CONTENT_PHASE_MAP = {
|
||||
"动画制作": "中期",
|
||||
"AI配音": "后期", "音效": "后期",
|
||||
"修补镜头": "后期", "剪辑": "后期", "音乐/BGM": "后期", "字幕": "后期",
|
||||
"培训": "内部事务", "招聘面试": "内部事务", "内部其他": "内部事务",
|
||||
}
|
||||
|
||||
# 项目级内容类型(不需要选集数)
|
||||
PROJECT_LEVEL_TYPES = {"策划案", "大纲/梗概", "概念设计图", "测试片"}
|
||||
PROJECT_LEVEL_TYPES = {"策划案", "大纲/梗概", "概念设计图", "测试片", "培训", "招聘面试", "内部其他"}
|
||||
|
||||
@ -26,7 +26,10 @@ def get_dashboard(
|
||||
):
|
||||
"""全局仪表盘数据"""
|
||||
# 项目概览
|
||||
active = db.query(Project).filter(Project.status == ProjectStatus.IN_PROGRESS).all()
|
||||
active = db.query(Project).filter(
|
||||
Project.status == ProjectStatus.IN_PROGRESS,
|
||||
Project.name != "内部事务",
|
||||
).all()
|
||||
completed = db.query(Project).filter(Project.status == ProjectStatus.COMPLETED).all()
|
||||
abandoned = db.query(Project).filter(Project.status == ProjectStatus.ABANDONED).all()
|
||||
|
||||
|
||||
@ -88,9 +88,9 @@ def create_submission(
|
||||
raise HTTPException(status_code=422, detail="请填写投入时长")
|
||||
|
||||
# 产出时长校验:前期内容不需要,中期/后期内容必须 > 0
|
||||
PRE_PHASE_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'}
|
||||
NO_DURATION_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:
|
||||
if content_val not in NO_DURATION_TYPES:
|
||||
total_secs = (req.duration_minutes or 0) * 60 + (req.duration_seconds or 0)
|
||||
if total_secs <= 0:
|
||||
raise HTTPException(status_code=422, detail="请填写产出时长")
|
||||
@ -219,7 +219,7 @@ def update_submission(
|
||||
sub.total_seconds = (sub.duration_minutes or 0) * 60 + (sub.duration_seconds or 0)
|
||||
|
||||
# 产出时长校验:前期内容不需要,中期/后期内容必须 > 0
|
||||
PRE_PHASE_TYPES = {'策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'}
|
||||
NO_DURATION_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="请填写产出时长")
|
||||
|
||||
@ -19,6 +19,8 @@ from calculations import (
|
||||
)
|
||||
from services.ai_service import generate_report_summary
|
||||
|
||||
INTERNAL_PROJECT_NAME = "内部事务"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -88,7 +90,8 @@ def generate_daily_report(db: Session) -> dict:
|
||||
|
||||
# 进行中项目
|
||||
active_projects = db.query(Project).filter(
|
||||
Project.status == ProjectStatus.IN_PROGRESS
|
||||
Project.status == ProjectStatus.IN_PROGRESS,
|
||||
Project.name != INTERNAL_PROJECT_NAME,
|
||||
).all()
|
||||
|
||||
projects_data = []
|
||||
@ -190,7 +193,8 @@ def generate_weekly_report(db: Session) -> dict:
|
||||
avg_daily = round(week_total_secs / max(1, len(week_submitter_ids)) / max(1, working_days), 1)
|
||||
|
||||
active_projects = db.query(Project).filter(
|
||||
Project.status == ProjectStatus.IN_PROGRESS
|
||||
Project.status == ProjectStatus.IN_PROGRESS,
|
||||
Project.name != INTERNAL_PROJECT_NAME,
|
||||
).all()
|
||||
|
||||
projects_data = []
|
||||
@ -340,7 +344,8 @@ def generate_monthly_report(db: Session) -> dict:
|
||||
month_submitters = set(s.user_id for s in month_subs)
|
||||
|
||||
all_projects = db.query(Project).filter(
|
||||
Project.status.in_([ProjectStatus.IN_PROGRESS, ProjectStatus.COMPLETED])
|
||||
Project.status.in_([ProjectStatus.IN_PROGRESS, ProjectStatus.COMPLETED]),
|
||||
Project.name != INTERNAL_PROJECT_NAME,
|
||||
).all()
|
||||
|
||||
completed_this_month = [
|
||||
@ -467,7 +472,8 @@ def analyze_project_risks(db: Session) -> list:
|
||||
"""
|
||||
today = date.today()
|
||||
active_projects = db.query(Project).filter(
|
||||
Project.status == ProjectStatus.IN_PROGRESS
|
||||
Project.status == ProjectStatus.IN_PROGRESS,
|
||||
Project.name != INTERNAL_PROJECT_NAME,
|
||||
).all()
|
||||
|
||||
risks = []
|
||||
|
||||
@ -70,29 +70,31 @@
|
||||
<el-option v-for="p in activeProjects" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目阶段" required>
|
||||
<el-select v-model="form.project_phase" style="width:100%">
|
||||
<el-option label="前期" value="前期" />
|
||||
<el-option label="中期" value="中期" />
|
||||
<el-option label="后期" value="后期" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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="QC" value="QC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="内容类型" required>
|
||||
<el-select v-model="form.content_type" placeholder="本次提交的产出类型" style="width:100%">
|
||||
<template v-if="!isInternalProject">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目阶段" required>
|
||||
<el-select v-model="form.project_phase" style="width:100%">
|
||||
<el-option label="前期" value="前期" />
|
||||
<el-option label="中期" value="中期" />
|
||||
<el-option label="后期" value="后期" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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="QC" value="QC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-form-item :label="isInternalProject ? '事务类型' : '内容类型'" required>
|
||||
<el-select v-model="form.content_type" :placeholder="isInternalProject ? '选择事务类型' : '本次提交的产出类型'" style="width:100%">
|
||||
<el-option v-for="ct in filteredContentTypes" :key="ct" :label="ct" :value="ct" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@ -157,28 +159,30 @@
|
||||
<el-form-item label="所属项目">
|
||||
<el-input :model-value="editForm._project_name" disabled />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目阶段" required>
|
||||
<el-select v-model="editForm.project_phase" style="width:100%">
|
||||
<el-option label="前期" value="前期" />
|
||||
<el-option label="中期" value="中期" />
|
||||
<el-option label="后期" value="后期" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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="QC" value="QC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="内容类型" required>
|
||||
<template v-if="!isEditInternalProject">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目阶段" required>
|
||||
<el-select v-model="editForm.project_phase" style="width:100%">
|
||||
<el-option label="前期" value="前期" />
|
||||
<el-option label="中期" value="中期" />
|
||||
<el-option label="后期" value="后期" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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="QC" value="QC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-form-item :label="isEditInternalProject ? '事务类型' : '内容类型'" required>
|
||||
<el-select v-model="editForm.content_type" style="width:100%">
|
||||
<el-option v-for="ct in editFilteredContentTypes" :key="ct" :label="ct" :value="ct" />
|
||||
</el-select>
|
||||
@ -250,6 +254,7 @@ const CONTENT_PHASE_MAP = {
|
||||
'剧本': '前期', '分镜': '前期', '人设图': '前期', '场景图': '前期', '道具图': '前期',
|
||||
'动画制作': '中期',
|
||||
'AI配音': '后期', '音效': '后期', '修补镜头': '后期', '剪辑': '后期', '音乐/BGM': '后期', '字幕': '后期',
|
||||
'培训': '内部事务', '招聘面试': '内部事务', '内部其他': '内部事务',
|
||||
}
|
||||
|
||||
// 按阶段分组的内容类型(有序)
|
||||
@ -257,16 +262,17 @@ const PHASE_CONTENT_TYPES = {
|
||||
'前期': ['策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'],
|
||||
'中期': ['动画制作'],
|
||||
'后期': ['AI配音', '音效', '修补镜头', '剪辑', '音乐/BGM', '字幕'],
|
||||
'内部事务': ['培训', '招聘面试', '内部其他'],
|
||||
}
|
||||
|
||||
// 项目级内容类型(不需要选集数)
|
||||
const PROJECT_LEVEL_TYPES = new Set(['策划案', '大纲/梗概', '概念设计图', '测试片'])
|
||||
const PROJECT_LEVEL_TYPES = new Set(['策划案', '大纲/梗概', '概念设计图', '测试片', '培训', '招聘面试', '内部其他'])
|
||||
function needsEpisode(contentType) {
|
||||
return !PROJECT_LEVEL_TYPES.has(contentType)
|
||||
}
|
||||
|
||||
// 前期内容不显示产出时长
|
||||
const NO_DURATION_TYPES = new Set(PHASE_CONTENT_TYPES['前期'])
|
||||
// 前期 + 内部事务不显示产出时长
|
||||
const NO_DURATION_TYPES = new Set([...PHASE_CONTENT_TYPES['前期'], ...PHASE_CONTENT_TYPES['内部事务']])
|
||||
function showDuration(contentType) {
|
||||
return !NO_DURATION_TYPES.has(contentType)
|
||||
}
|
||||
@ -310,9 +316,23 @@ const editForm = reactive({
|
||||
episode_number: null,
|
||||
})
|
||||
|
||||
// 根据阶段过滤内容类型
|
||||
const filteredContentTypes = computed(() => PHASE_CONTENT_TYPES[form.project_phase] || [])
|
||||
// 内部事务项目检测
|
||||
const INTERNAL_PROJECT_NAME = '内部事务'
|
||||
const isInternalProject = computed(() => {
|
||||
const proj = activeProjects.value.find(p => p.id === form.project_id)
|
||||
return proj?.name === INTERNAL_PROJECT_NAME
|
||||
})
|
||||
const isEditInternalProject = computed(() => {
|
||||
return editForm._project_name === INTERNAL_PROJECT_NAME
|
||||
})
|
||||
|
||||
// 根据阶段过滤内容类型(内部事务项目只显示内部事务类型)
|
||||
const filteredContentTypes = computed(() => {
|
||||
if (isInternalProject.value) return PHASE_CONTENT_TYPES['内部事务']
|
||||
return PHASE_CONTENT_TYPES[form.project_phase] || []
|
||||
})
|
||||
const editFilteredContentTypes = computed(() => {
|
||||
if (isEditInternalProject.value) return PHASE_CONTENT_TYPES['内部事务']
|
||||
const types = PHASE_CONTENT_TYPES[editForm.project_phase] || []
|
||||
// 编辑时如果当前值不在列表中(旧数据兼容),也加入
|
||||
if (editForm.content_type && !types.includes(editForm.content_type)) {
|
||||
@ -321,6 +341,17 @@ const editFilteredContentTypes = computed(() => {
|
||||
return types
|
||||
})
|
||||
|
||||
// 选择内部事务项目时自动设置默认值
|
||||
watch(() => form.project_id, () => {
|
||||
if (isInternalProject.value) {
|
||||
form.project_phase = '内部事务'
|
||||
form.work_type = '制作'
|
||||
form.content_type = PHASE_CONTENT_TYPES['内部事务'][0]
|
||||
form.duration_minutes = 0
|
||||
form.duration_seconds = 0
|
||||
}
|
||||
})
|
||||
|
||||
// 切换阶段时重置内容类型为该阶段第一个
|
||||
watch(() => form.project_phase, (phase) => {
|
||||
const types = PHASE_CONTENT_TYPES[phase]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user