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]