diff --git a/backend/calculations.py b/backend/calculations.py
index b955524..18cd0b4 100644
--- a/backend/calculations.py
+++ b/backend/calculations.py
@@ -8,7 +8,7 @@ from collections import defaultdict
from datetime import date, timedelta
from models import (
User, Project, Submission, AIToolCost, AIToolCostAllocation,
- OutsourceCost, CostOverride, OverheadCost, WorkType, CostAllocationType
+ OutsourceCost, CostOverride, OverheadCost, WorkType, PhaseGroup, CostAllocationType
)
from config import WORKING_DAYS_PER_MONTH
@@ -371,6 +371,7 @@ def calc_team_efficiency(project_id: int, db: Session) -> list:
).filter(
Submission.project_id == project_id,
Submission.total_seconds > 0,
+ Submission.project_phase == PhaseGroup.PRODUCTION, # 只算中期
).group_by(Submission.user_id).all()
if not per_user:
diff --git a/backend/main.py b/backend/main.py
index 7975efe..c762d65 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -145,8 +145,10 @@ def init_roles_and_admin():
try:
conn.execute(text("""
ALTER TABLE submissions MODIFY COLUMN content_type
- ENUM('PLANNING','SCRIPT','STORYBOARD','CHARACTER_DESIGN','SCENE_DESIGN',
- 'ANIMATION','DUBBING','SOUND_EFFECTS','SHOT_REPAIR','EDITING','OTHER',
+ ENUM('PLANNING','SYNOPSIS','CONCEPT_DESIGN','TEST_FOOTAGE',
+ 'SCRIPT','STORYBOARD','CHARACTER_DESIGN','SCENE_DESIGN',
+ 'PROP_DESIGN','ANIMATION','DUBBING','AI_DUBBING','SOUND_EFFECTS',
+ 'SHOT_REPAIR','EDITING','MUSIC','SUBTITLE','OTHER',
'DESIGN') NOT NULL
"""))
conn.commit()
@@ -173,6 +175,24 @@ def init_roles_and_admin():
except Exception as e:
print(f"[MIGRATE] schema migration error (non-fatal): {e}")
+ # 一次性迁移:阶段"制作"→"中期"
+ try:
+ phase_tables = [
+ ("submissions", "project_phase"),
+ ("projects", "current_phase"),
+ ("users", "phase_group"),
+ ("project_milestones", "phase"),
+ ]
+ total_migrated = 0
+ for table, col in phase_tables:
+ r = conn.execute(text(f"UPDATE {table} SET {col} = '中期' WHERE {col} = '制作'"))
+ total_migrated += r.rowcount or 0
+ conn.commit()
+ if total_migrated > 0:
+ print(f"[MIGRATE] renamed phase '制作'→'中期' in {total_migrated} rows")
+ except Exception as e:
+ print(f"[MIGRATE] phase rename error (non-fatal): {e}")
+
# 初始化 / 同步内置角色权限
for role_name, role_def in BUILTIN_ROLES.items():
existing = db.query(Role).filter(Role.name == role_name).first()
diff --git a/backend/models.py b/backend/models.py
index 4c0f2ad..3a57534 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -118,7 +118,7 @@ class ProjectStatus(str, enum.Enum):
class PhaseGroup(str, enum.Enum):
PRE = "前期"
- PRODUCTION = "制作"
+ PRODUCTION = "中期"
POST = "后期"
@@ -131,19 +131,26 @@ class WorkType(str, enum.Enum):
class ContentType(str, enum.Enum):
- # 前期
+ # 前期(项目级)
PLANNING = "策划案"
+ SYNOPSIS = "大纲/梗概"
+ CONCEPT_DESIGN = "概念设计图"
+ TEST_FOOTAGE = "测试片"
+ # 前期(集数级)
SCRIPT = "剧本"
STORYBOARD = "分镜"
CHARACTER_DESIGN = "人设图"
SCENE_DESIGN = "场景图"
- # 制作
+ PROP_DESIGN = "道具图"
+ # 中期
ANIMATION = "动画制作"
# 后期
- DUBBING = "配音"
+ DUBBING = "AI配音"
SOUND_EFFECTS = "音效"
SHOT_REPAIR = "修补镜头"
EDITING = "剪辑"
+ MUSIC = "音乐/BGM"
+ SUBTITLE = "字幕"
# 通用
OTHER = "其他"
@@ -415,18 +422,27 @@ DEFAULT_MILESTONES = [
{"name": "分镜", "phase": "前期", "sort_order": 3},
{"name": "人设图", "phase": "前期", "sort_order": 4},
{"name": "场景图", "phase": "前期", "sort_order": 5},
+ {"name": "道具图", "phase": "前期", "sort_order": 6},
# 后期
{"name": "配音", "phase": "后期", "sort_order": 1},
- {"name": "音效", "phase": "后期", "sort_order": 2},
- {"name": "修补镜头", "phase": "后期", "sort_order": 3},
- {"name": "剪辑", "phase": "后期", "sort_order": 4},
- {"name": "杂项", "phase": "后期", "sort_order": 5},
+ {"name": "AI配音", "phase": "后期", "sort_order": 2},
+ {"name": "音效", "phase": "后期", "sort_order": 3},
+ {"name": "修补镜头", "phase": "后期", "sort_order": 4},
+ {"name": "剪辑", "phase": "后期", "sort_order": 5},
+ {"name": "音乐/BGM", "phase": "后期", "sort_order": 6},
+ {"name": "字幕", "phase": "后期", "sort_order": 7},
+ {"name": "杂项", "phase": "后期", "sort_order": 8},
]
# 内容类型 → 阶段映射(用于自动设置阶段和关联里程碑)
CONTENT_PHASE_MAP = {
- "策划案": "前期", "剧本": "前期", "分镜": "前期",
- "人设图": "前期", "场景图": "前期",
- "动画制作": "制作",
- "配音": "后期", "音效": "后期", "修补镜头": "后期", "剪辑": "后期",
+ "策划案": "前期", "大纲/梗概": "前期", "概念设计图": "前期", "测试片": "前期",
+ "剧本": "前期", "分镜": "前期",
+ "人设图": "前期", "场景图": "前期", "道具图": "前期",
+ "动画制作": "中期",
+ "AI配音": "后期", "音效": "后期",
+ "修补镜头": "后期", "剪辑": "后期", "音乐/BGM": "后期", "字幕": "后期",
}
+
+# 项目级内容类型(不需要选集数)
+PROJECT_LEVEL_TYPES = {"策划案", "大纲/梗概", "概念设计图", "测试片"}
diff --git a/backend/routers/projects.py b/backend/routers/projects.py
index a29896d..6efd5a4 100644
--- a/backend/routers/projects.py
+++ b/backend/routers/projects.py
@@ -97,27 +97,38 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
if len(pre_ms) > 0 and pre_completed < len(pre_ms):
current_stage = "前期"
elif progress < 100:
- current_stage = "制作"
+ current_stage = "中期"
elif len(post_ms) > 0 and post_completed < len(post_ms):
current_stage = "后期"
else:
current_stage = "已完成"
- # EP 集数进度
+ # EP 集数进度(批量查询,避免 N+1)
episode_progress = []
ep_target = p.episode_duration_minutes * 60 # 每集目标秒数
+ # 一次查出所有有提交的集数数据
+ ep_rows = db.query(
+ Submission.episode_number,
+ User.name,
+ sa_func.sum(Submission.total_seconds).label("secs"),
+ ).join(User, User.id == Submission.user_id, isouter=True).filter(
+ Submission.project_id == p.id,
+ Submission.episode_number.isnot(None),
+ Submission.total_seconds > 0,
+ ).group_by(Submission.episode_number, User.name).all()
+
+ # 按集数聚合
+ ep_data = {} # {ep: {total, contributors: {name: secs}}}
+ for ep_num, user_name, secs in ep_rows:
+ if ep_num not in ep_data:
+ ep_data[ep_num] = {"total": 0, "contributors": {}}
+ ep_data[ep_num]["total"] += secs
+ name = user_name or "未知"
+ ep_data[ep_num]["contributors"][name] = ep_data[ep_num]["contributors"].get(name, 0) + secs
+
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
+ info = ep_data.get(ep, {"total": 0, "contributors": {}})
+ ep_total = info["total"]
episode_progress.append({
"episode": ep,
"total_seconds": round(ep_total, 1),
@@ -125,7 +136,7 @@ def enrich_project(p: Project, db: Session) -> ProjectOut:
"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])
+ for k, v in sorted(info["contributors"].items(), key=lambda x: -x[1])
],
})
diff --git a/backend/routers/submissions.py b/backend/routers/submissions.py
index 6068b69..e01907f 100644
--- a/backend/routers/submissions.py
+++ b/backend/routers/submissions.py
@@ -82,6 +82,20 @@ def create_submission(
raise HTTPException(status_code=404, detail="目标用户不存在")
target_user_id = req.user_id
+ # 必填校验
+ if not req.description or not req.description.strip():
+ raise HTTPException(status_code=422, detail="请填写描述")
+ 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:
+ raise HTTPException(status_code=422, detail="请选择集数")
+
# 校验项目存在
project = db.query(Project).filter(Project.id == req.project_id).first()
if not project:
@@ -251,3 +265,32 @@ def delete_submission(
db.delete(sub)
db.commit()
return {"detail": "删除成功"}
+
+
+@router.get("/daily-hours")
+def get_daily_hours(
+ user_id: Optional[int] = Query(None),
+ target_date: Optional[date] = Query(None, alias="date"),
+ db: Session = Depends(get_db),
+ current_user: User = Depends(get_current_user)
+):
+ """查询指定用户某日已填工时"""
+ from sqlalchemy import func
+
+ uid = user_id or current_user.id
+ # 非本人查询需要 proxy 权限
+ if uid != current_user.id and not current_user.has_permission("submission:proxy"):
+ raise HTTPException(status_code=403, detail="无权查看他人工时")
+
+ d = target_date or date.today()
+ filled = db.query(func.sum(Submission.hours_spent)).filter(
+ Submission.user_id == uid,
+ Submission.submit_date == d,
+ ).scalar() or 0.0
+
+ target = 8.0
+ return {
+ "filled": round(filled, 1),
+ "target": target,
+ "remaining": round(max(target - filled, 0), 1),
+ }
diff --git a/backend/schemas.py b/backend/schemas.py
index edd3389..7adfd50 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -22,7 +22,7 @@ class UserCreate(BaseModel):
username: str
password: str
name: str
- phase_group: str # 前期/制作/后期
+ phase_group: str # 前期/中期/后期
role_id: int
monthly_salary: float = 0
bonus: float = 0
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 0f6e8b8..ecccf34 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -79,6 +79,7 @@ export const submissionApi = {
update: (id, data) => api.put(`/submissions/${id}`, data),
delete: (id) => api.delete(`/submissions/${id}`),
history: (id) => api.get(`/submissions/${id}/history`),
+ dailyHours: (params) => api.get('/submissions/daily-hours', { params }),
}
// ── 成本 ──
diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue
index 36f0fc8..ba0ac78 100644
--- a/frontend/src/views/ProjectDetail.vue
+++ b/frontend/src/views/ProjectDetail.vue
@@ -93,7 +93,7 @@
- 制作
+ 中期
{{ project.progress_percent }}%
@@ -140,9 +140,9 @@
-
+
-
+
@@ -409,7 +409,7 @@
-
+
@@ -550,7 +550,7 @@ function initProgressChart() {
function handleProgressResize() { progressChart?.resize() }
const editForm = reactive({
- name: '', project_type: '客户正式项目', status: '制作中', leader_id: null, current_phase: '制作',
+ name: '', project_type: '客户正式项目', status: '制作中', leader_id: null, current_phase: '中期',
episode_duration_minutes: 5, episode_count: 1,
estimated_completion_date: null, contract_amount: null,
})
@@ -560,7 +560,7 @@ const stageColor = computed(() => {
const s = project.value.current_stage
if (s === '已完成') return '#34C759'
if (s === '前期') return '#8F959E'
- if (s === '制作') return '#3370FF'
+ if (s === '中期') return '#3370FF'
if (s === '后期') return '#FF9500'
return 'var(--text-primary)'
})
@@ -798,7 +798,7 @@ async function openEdit() {
}
Object.assign(editForm, {
name: p.name, project_type: p.project_type, status: p.status || '制作中', leader_id: p.leader_id,
- current_phase: p.current_phase || '制作',
+ current_phase: p.current_phase || '中期',
episode_duration_minutes: p.episode_duration_minutes, episode_count: p.episode_count,
estimated_completion_date: p.estimated_completion_date, contract_amount: p.contract_amount,
})
diff --git a/frontend/src/views/Projects.vue b/frontend/src/views/Projects.vue
index ec55b03..3a5bc5e 100644
--- a/frontend/src/views/Projects.vue
+++ b/frontend/src/views/Projects.vue
@@ -183,7 +183,7 @@ function stageLabel(row) {
if (!s) return row.progress_percent + '%'
const stage = row.current_stage
if (stage === '前期') return `前期 ${s.pre.completed}/${s.pre.total}`
- if (stage === '制作') return `制作 ${row.progress_percent}%`
+ if (stage === '中期') return `中期 ${row.progress_percent}%`
if (stage === '后期') return `后期 ${s.post.completed}/${s.post.total}`
if (stage === '已完成') return '已完成'
return row.progress_percent + '%'
@@ -231,7 +231,7 @@ onMounted(async () => {
background: var(--bg-hover); color: var(--text-secondary);
}
.stage-tag.stage-前期 { background: #F0F1F5; color: #8F959E; }
-.stage-tag.stage-制作 { background: #EBF1FF; color: #3370FF; }
+.stage-tag.stage-中期 { background: #EBF1FF; color: #3370FF; }
.stage-tag.stage-后期 { background: #FFF3E0; color: #FF9500; }
.stage-tag.stage-已完成 { background: #E8F8EE; color: #34C759; }
.rate-badge {
diff --git a/frontend/src/views/Settlement.vue b/frontend/src/views/Settlement.vue
index a039d30..3fbdada 100644
--- a/frontend/src/views/Settlement.vue
+++ b/frontend/src/views/Settlement.vue
@@ -76,7 +76,7 @@
-
制作(秒数制)
+
中期(秒数制)
测试损耗
{{ formatSecs(data.test_waste_seconds) }}
diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue
index 5860bfc..caad223 100644
--- a/frontend/src/views/Submissions.vue
+++ b/frontend/src/views/Submissions.vue
@@ -8,6 +8,9 @@
+
+
+
@@ -16,6 +19,18 @@
+
+
+ 今日工时:{{ dailyHours.filled }}h / {{ dailyHours.target }}h
+
+
+
@@ -27,7 +42,7 @@
{{ row.work_type }}
-
+
{{ row.total_seconds > 0 ? formatSecs(row.total_seconds) : '—' }}
@@ -46,7 +61,7 @@
-
+
@@ -60,7 +75,7 @@
-
+
@@ -71,39 +86,22 @@
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
分
@@ -112,11 +110,13 @@
无产出秒数的工作可填 0
-
+
小时
- (选填)实际花费的工作时间
+
+ 今日已填 {{ dialogDailyHours.filled }}h / 8h,还剩 {{ dialogDailyHours.remaining }}h
+
@@ -135,8 +135,8 @@
-
-
+
+
@@ -159,7 +159,7 @@
-
+
@@ -170,39 +170,22 @@
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
分
@@ -210,7 +193,7 @@
秒
-
+
小时
@@ -224,7 +207,7 @@
-
+
@@ -259,9 +242,29 @@ import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const CONTENT_PHASE_MAP = {
- '策划案': '前期', '剧本': '前期', '分镜': '前期', '人设图': '前期', '场景图': '前期',
- '动画制作': '制作',
- '配音': '后期', '音效': '后期', '修补镜头': '后期', '剪辑': '后期',
+ '策划案': '前期', '大纲/梗概': '前期', '概念设计图': '前期', '测试片': '前期',
+ '剧本': '前期', '分镜': '前期', '人设图': '前期', '场景图': '前期', '道具图': '前期',
+ '动画制作': '中期',
+ 'AI配音': '后期', '音效': '后期', '修补镜头': '后期', '剪辑': '后期', '音乐/BGM': '后期', '字幕': '后期',
+}
+
+// 按阶段分组的内容类型(有序)
+const PHASE_CONTENT_TYPES = {
+ '前期': ['策划案', '大纲/梗概', '概念设计图', '测试片', '剧本', '分镜', '人设图', '场景图', '道具图'],
+ '中期': ['动画制作'],
+ '后期': ['AI配音', '音效', '修补镜头', '剪辑', '音乐/BGM', '字幕'],
+}
+
+// 项目级内容类型(不需要选集数)
+const PROJECT_LEVEL_TYPES = new Set(['策划案', '大纲/梗概', '概念设计图', '测试片'])
+function needsEpisode(contentType) {
+ return !PROJECT_LEVEL_TYPES.has(contentType)
+}
+
+// 前期内容不显示产出时长
+const NO_DURATION_TYPES = new Set(PHASE_CONTENT_TYPES['前期'])
+function showDuration(contentType) {
+ return !NO_DURATION_TYPES.has(contentType)
}
const loading = ref(false)
@@ -271,26 +274,86 @@ const submissions = ref([])
const projects = ref([])
const users = ref([])
const dateRange = ref(null)
-const filter = reactive({ project_id: null, start_date: null, end_date: null })
+const filter = reactive({ project_id: null, user_id: null, start_date: null, end_date: null })
const activeProjects = computed(() => projects.value.filter(p => p.status === '制作中'))
const today = new Date().toISOString().split('T')[0]
const form = reactive({
- project_id: null, project_phase: '制作', work_type: '制作',
+ 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,
})
-// 选择内容类型时自动设置对应的项目阶段
-watch(() => form.content_type, (val) => {
- if (CONTENT_PHASE_MAP[val]) {
- form.project_phase = CONTENT_PHASE_MAP[val]
+// ── 编辑逻辑 ──
+const showEdit = ref(false)
+const editing = ref(false)
+const editForm = reactive({
+ _id: null,
+ _project_name: '',
+ _project_id: null,
+ project_phase: '',
+ work_type: '',
+ content_type: '',
+ duration_minutes: 0,
+ duration_seconds: 0,
+ hours_spent: null,
+ submit_to: '',
+ description: '',
+ submit_date: '',
+ change_reason: '',
+ episode_number: null,
+})
+
+// 根据阶段过滤内容类型
+const filteredContentTypes = computed(() => PHASE_CONTENT_TYPES[form.project_phase] || [])
+const editFilteredContentTypes = computed(() => {
+ const types = PHASE_CONTENT_TYPES[editForm.project_phase] || []
+ // 编辑时如果当前值不在列表中(旧数据兼容),也加入
+ if (editForm.content_type && !types.includes(editForm.content_type)) {
+ return [editForm.content_type, ...types]
+ }
+ return types
+})
+
+// 切换阶段时重置内容类型为该阶段第一个
+watch(() => form.project_phase, (phase) => {
+ const types = PHASE_CONTENT_TYPES[phase]
+ if (types && !types.includes(form.content_type)) {
+ form.content_type = types[0]
}
})
-// 里程碑超期检测(利用 projectApi.list 返回的 milestones 数据)
+watch(() => editForm.project_phase, (phase) => {
+ const types = PHASE_CONTENT_TYPES[phase]
+ if (types && !types.includes(editForm.content_type)) {
+ editForm.content_type = types[0]
+ }
+})
+
+// 内容类型变化时:前期隐藏产出时长清零 + 项目级清空集数
+watch(() => form.content_type, (val) => {
+ if (NO_DURATION_TYPES.has(val)) {
+ form.duration_minutes = 0
+ form.duration_seconds = 0
+ }
+ if (!needsEpisode(val)) {
+ form.episode_number = null
+ }
+})
+
+watch(() => editForm.content_type, (val) => {
+ if (NO_DURATION_TYPES.has(val)) {
+ editForm.duration_minutes = 0
+ editForm.duration_seconds = 0
+ }
+ if (!needsEpisode(val)) {
+ editForm.episode_number = null
+ }
+})
+
+// 里程碑超期检测
const selectedProject = computed(() =>
projects.value.find(p => p.id === form.project_id) || null
)
@@ -312,6 +375,24 @@ const episodeOptions = computed(() => {
return Array.from({length: proj.episode_count}, (_, i) => i + 1)
})
+// ── 今日工时 ──
+const dailyHours = ref(null)
+const dialogDailyHours = ref(null)
+
+async function loadDailyHours() {
+ try {
+ const params = { date: today }
+ if (form.user_id) params.user_id = form.user_id
+ dialogDailyHours.value = await submissionApi.dailyHours(params)
+ } catch { /* ignore */ }
+}
+
+async function loadListDailyHours() {
+ try {
+ dailyHours.value = await submissionApi.dailyHours({ date: today })
+ } catch { /* ignore */ }
+}
+
function formatSecs(s) {
if (!s) return '0秒'
const m = Math.floor(s / 60)
@@ -332,6 +413,9 @@ 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 (!form.description?.trim()) { ElMessage.warning('请填写描述'); return }
+ if (!form.hours_spent || form.hours_spent <= 0) { ElMessage.warning('请填写投入时长'); return }
if (isMilestoneOverdue.value && !form.delay_reason?.trim()) {
ElMessage.warning('该里程碑已超期,请填写延期原因')
return
@@ -350,29 +434,15 @@ async function handleCreate() {
form.episode_number = null
form.user_id = null
load()
+ // 刷新工时进度
+ loadListDailyHours()
+ const updated = await submissionApi.dailyHours({ date: today })
+ if (updated && updated.remaining > 0) {
+ ElMessage.info(`今日还有 ${updated.remaining}h 工时未填报`)
+ }
} finally { creating.value = false }
}
-// ── 编辑逻辑 ──
-const showEdit = ref(false)
-const editing = ref(false)
-const editForm = reactive({
- _id: null,
- _project_name: '',
- _project_id: null,
- project_phase: '',
- work_type: '',
- content_type: '',
- duration_minutes: 0,
- duration_seconds: 0,
- hours_spent: null,
- submit_to: '',
- 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 []
@@ -432,8 +502,14 @@ async function handleDeleteFromEdit() {
}
}
+// 打开新增弹窗时加载工时
+watch(showCreate, (val) => {
+ if (val) loadDailyHours()
+})
+
onMounted(async () => {
load()
+ loadListDailyHours()
try { projects.value = await projectApi.list({}) } catch {}
if (authStore.hasPermission('submission:proxy')) {
try { users.value = await userApi.brief() } catch {}
@@ -448,4 +524,6 @@ onMounted(async () => {
.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); }
+.daily-hours-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 10px 16px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
+.daily-label { font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue
index 2718497..03f46be 100644
--- a/frontend/src/views/Users.vue
+++ b/frontend/src/views/Users.vue
@@ -54,7 +54,7 @@
-
+
@@ -119,7 +119,7 @@ const sortedUsers = computed(() => {
})
})
const form = reactive({
- username: '', password: '', name: '', phase_group: '制作', role_id: null,
+ username: '', password: '', name: '', phase_group: '中期', role_id: null,
monthly_salary: 0, bonus: 0, social_insurance: 0, is_active: 1,
})