From becfd74efddbc5fea01207811fcb22fcdf88a366 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Tue, 10 Mar 2026 10:18:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=86=85=E9=83=A8=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=8A=9F=E8=83=BD=EF=BC=88=E5=9F=B9=E8=AE=AD?= =?UTF-8?q?/=E6=8B=9B=E8=81=98=E9=9D=A2=E8=AF=95/=E5=86=85=E9=83=A8?= =?UTF-8?q?=E5=85=B6=E4=BB=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ContentType: TRAINING, RECRUITMENT, INTERNAL_OTHER - 新增 PhaseGroup: INTERNAL (内部事务) - 前端选"内部事务"项目时隐藏项目阶段/工作类型/产出时长 - 内容类型改显示为"事务类型"(培训/招聘面试/内部其他) - 报告和仪表盘中排除内部事务项目 - 内部事务成本后续按管理成本逻辑分摊到所有项目 Co-Authored-By: Claude Opus 4.6 --- backend/main.py | 13 ++- backend/models.py | 8 +- backend/routers/dashboard.py | 5 +- backend/routers/submissions.py | 6 +- backend/services/report_service.py | 14 ++- frontend/src/views/Submissions.vue | 131 ++++++++++++++++++----------- 6 files changed, 117 insertions(+), 60 deletions(-) diff --git a/backend/main.py b/backend/main.py index 5e3e727..b67059f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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(""" diff --git a/backend/models.py b/backend/models.py index b8000b1..c1f3ac8 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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 = {"策划案", "大纲/梗概", "概念设计图", "测试片", "培训", "招聘面试", "内部其他"} diff --git a/backend/routers/dashboard.py b/backend/routers/dashboard.py index 7272789..6042ff8 100644 --- a/backend/routers/dashboard.py +++ b/backend/routers/dashboard.py @@ -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() diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py index 638e6de..43621a3 100644 --- a/backend/routers/submissions.py +++ b/backend/routers/submissions.py @@ -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="请填写产出时长") diff --git a/backend/services/report_service.py b/backend/services/report_service.py index 3ad40d4..e0c7725 100644 --- a/backend/services/report_service.py +++ b/backend/services/report_service.py @@ -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 = [] diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue index c4741c2..2a74440 100644 --- a/frontend/src/views/Submissions.vue +++ b/frontend/src/views/Submissions.vue @@ -70,29 +70,31 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -157,28 +159,30 @@ - - - - - - - - - - - - - - - - - - - - - - + + @@ -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]