From a43bed1d641de6175f3600353bf14cd85838f618 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Wed, 25 Feb 2026 14:13:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E7=B2=BE=E7=BB=86?= =?UTF-8?q?=E5=8C=96=20+=20=E6=88=90=E6=9C=AC=E4=BA=BA=E5=8A=9B=E8=B0=83?= =?UTF-8?q?=E6=95=B4=20+=20=E4=BB=AA=E8=A1=A8=E7=9B=98=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户管理页面权限拆分:user:view可查看列表,user:manage控制新增/编辑,user:view_cost控制薪资列显示 - 成本管理新增"人力调整"Tab,支持查看和录入CostOverride记录 - 仪表盘新增4个子权限(成本/损耗/盈亏/风险预警),管理员可按角色灵活配置可见模块 - 修复组长进入成本管理页面弹出"权限不足"提示的问题 - 修复主管无法访问用户管理页面的路由权限问题 - 提交页面延期原因字段仅在里程碑超期时显示 Co-Authored-By: Claude Opus 4.6 --- backend/models.py | 9 +- backend/routers/users.py | 22 +++-- frontend/src/components/Layout.vue | 2 +- frontend/src/router/index.js | 2 +- frontend/src/views/Costs.vue | 75 +++++++++++++- frontend/src/views/Dashboard.vue | 24 ++--- frontend/src/views/MemberDetail.vue | 6 +- frontend/src/views/Submissions.vue | 148 ++++++++++++++++++++++++++++ frontend/src/views/Users.vue | 18 ++-- 9 files changed, 270 insertions(+), 36 deletions(-) diff --git a/backend/models.py b/backend/models.py index 372ee7a..f0a8ee9 100644 --- a/backend/models.py +++ b/backend/models.py @@ -13,7 +13,11 @@ import enum ALL_PERMISSIONS = [ # 仪表盘 - ("dashboard:view", "查看仪表盘", "仪表盘"), + ("dashboard:view", "查看仪表盘(基础)", "仪表盘"), + ("dashboard:view_cost", "查看成本模块", "仪表盘"), + ("dashboard:view_waste", "查看损耗模块", "仪表盘"), + ("dashboard:view_profit", "查看盈亏模块", "仪表盘"), + ("dashboard:view_risk", "查看风险预警", "仪表盘"), # 项目管理 ("project:view", "查看项目", "项目管理"), ("project:create", "创建项目", "项目管理"), @@ -38,6 +42,7 @@ ALL_PERMISSIONS = [ ("cost_labor:create", "录入人力调整", "成本管理"), # 用户与角色 ("user:view", "查看用户列表", "用户与角色"), + ("user:view_cost", "查看成员薪资成本", "用户与角色"), ("user:manage", "管理用户", "用户与角色"), ("role:manage", "管理角色", "用户与角色"), # 结算与效率 @@ -66,7 +71,7 @@ BUILTIN_ROLES = { "主管": { "description": "管理项目和成本,不可管理用户和角色", "permissions": [ - "dashboard:view", + "dashboard:view", "dashboard:view_cost", "dashboard:view_waste", "dashboard:view_profit", "dashboard:view_risk", "project:view", "project:create", "project:edit", "project:complete", "submission:view", "submission:create", "cost_ai:view", "cost_ai:create", "cost_ai:delete", diff --git a/backend/routers/users.py b/backend/routers/users.py index c03916d..9df766d 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -10,27 +10,32 @@ from auth import get_current_user, hash_password, require_permission router = APIRouter(prefix="/api/users", tags=["用户管理"]) -def user_to_out(u: User) -> UserOut: +def user_to_out(u: User, hide_cost: bool = False) -> UserOut: return UserOut( id=u.id, username=u.username, name=u.name, phase_group=u.phase_group.value if hasattr(u.phase_group, 'value') else u.phase_group, role_id=u.role_id, role_name=u.role_name, permissions=u.permissions, - monthly_salary=u.monthly_salary, - bonus=u.bonus or 0, - social_insurance=u.social_insurance or 0, - monthly_total_cost=u.monthly_total_cost, - daily_cost=u.daily_cost, + monthly_salary=0 if hide_cost else u.monthly_salary, + bonus=0 if hide_cost else (u.bonus or 0), + social_insurance=0 if hide_cost else (u.social_insurance or 0), + monthly_total_cost=0 if hide_cost else u.monthly_total_cost, + daily_cost=0 if hide_cost else u.daily_cost, is_active=u.is_active, created_at=u.created_at, ) +def _can_view_cost(user: User) -> bool: + return "user:view_cost" in (user.permissions or []) + + @router.get("", response_model=List[UserOut]) def list_users( db: Session = Depends(get_db), current_user: User = Depends(require_permission("user:view")) ): + hide = not _can_view_cost(current_user) users = db.query(User).order_by(User.created_at.desc()).all() - return [user_to_out(u) for u in users] + return [user_to_out(u, hide_cost=hide) for u in users] @router.post("", response_model=UserOut) @@ -95,4 +100,5 @@ def get_user( user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="用户不存在") - return user_to_out(user) + hide = not _can_view_cost(current_user) + return user_to_out(user, hide_cost=hide) diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index bec14c3..4f73c9a 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -73,7 +73,7 @@ const menuItems = [ { path: '/projects', label: '项目管理', icon: 'FolderOpened', perm: 'project:view' }, { path: '/submissions', label: '内容提交', icon: 'EditPen', perm: 'submission:view' }, { path: '/costs', label: '成本管理', icon: 'Money', perm: ['cost_ai:view', 'cost_outsource:view', 'cost_overhead:view', 'cost_labor:view'] }, - { path: '/users', label: '用户管理', icon: 'User', perm: 'user:manage' }, + { path: '/users', label: '用户管理', icon: 'User', perm: 'user:view' }, { path: '/roles', label: '角色管理', icon: 'Lock', perm: 'role:manage' }, ] diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index da83176..ccbb5fe 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -12,7 +12,7 @@ const routes = [ { path: 'projects/:id', name: 'ProjectDetail', component: () => import('../views/ProjectDetail.vue'), meta: { perm: 'project:view' } }, { path: 'submissions', name: 'Submissions', component: () => import('../views/Submissions.vue'), meta: { perm: 'submission:view' } }, { path: 'costs', name: 'Costs', component: () => import('../views/Costs.vue'), meta: { perm: ['cost_ai:view', 'cost_outsource:view', 'cost_overhead:view', 'cost_labor:view'] } }, - { path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { perm: 'user:manage' } }, + { path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { perm: 'user:view' } }, { path: 'users/:id/detail', name: 'MemberDetail', component: () => import('../views/MemberDetail.vue'), meta: { perm: 'user:view' } }, { path: 'roles', name: 'Roles', component: () => import('../views/Roles.vue'), meta: { perm: 'role:manage' } }, { path: 'settlement/:id', name: 'Settlement', component: () => import('../views/Settlement.vue'), meta: { perm: 'settlement:view' } }, diff --git a/frontend/src/views/Costs.vue b/frontend/src/views/Costs.vue index 3b0cc2e..5837d89 100644 --- a/frontend/src/views/Costs.vue +++ b/frontend/src/views/Costs.vue @@ -62,6 +62,29 @@ + + + +
+ 新增调整 +
+ + + + + + + + + + + + + + + + +
@@ -155,47 +178,84 @@ 保存 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 8c71fb5..c7e1fb7 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -9,7 +9,7 @@
进行中项目
-
+
¥{{ formatNum(data.monthly_labor_cost) }}
@@ -30,7 +30,7 @@
人均日产出
-
+
@@ -41,7 +41,7 @@
已结算利润
-
+
@@ -52,7 +52,7 @@
利润率
-
+
@@ -67,7 +67,7 @@
-
+
@@ -95,11 +95,11 @@
-
+
近 30 天产出趋势
-
+
成本构成
@@ -132,7 +132,7 @@
{{ formatSecs(p.submitted_seconds) }} / {{ formatSecs(p.target_seconds) }} - + 损耗 {{ p.waste_rate }}% @@ -144,18 +144,18 @@
-
+
项目产出对比
-
+
损耗排行
-
+
项目盈亏分析 @@ -168,7 +168,7 @@
-
+
已结算项目
diff --git a/frontend/src/views/MemberDetail.vue b/frontend/src/views/MemberDetail.vue index 803197e..fc75a49 100644 --- a/frontend/src/views/MemberDetail.vue +++ b/frontend/src/views/MemberDetail.vue @@ -12,11 +12,11 @@
-
+
日成本 ¥{{ member.daily_cost || 0 }}
-
+
月总成本 ¥{{ (member.monthly_total_cost || 0).toLocaleString() }}
@@ -117,7 +117,9 @@ import { ref, computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { userApi, submissionApi } from '../api' +import { useAuthStore } from '../stores/auth' +const authStore = useAuthStore() const route = useRoute() const loading = ref(false) const member = ref({}) diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue index c68472f..9249d96 100644 --- a/frontend/src/views/Submissions.vue +++ b/frontend/src/views/Submissions.vue @@ -35,6 +35,11 @@ + + + @@ -130,6 +135,93 @@ 提交 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ +
+ + 小时 +
+
+ + + + + + + + + + + + + + + + + + +
+ +
@@ -137,6 +229,9 @@ import { ref, computed, onMounted, reactive, watch } from 'vue' import { submissionApi, projectApi } from '../api' import { ElMessage } from 'element-plus' +import { useAuthStore } from '../stores/auth' + +const authStore = useAuthStore() const CONTENT_PHASE_MAP = { '策划案': '前期', '剧本': '前期', '分镜': '前期', '人设图': '前期', '场景图': '前期', @@ -223,6 +318,59 @@ async function handleCreate() { } finally { creating.value = false } } +// ── 编辑逻辑 ── +const showEdit = ref(false) +const editing = ref(false) +const editForm = reactive({ + _id: null, + _project_name: '', + project_phase: '', + work_type: '', + content_type: '', + duration_minutes: 0, + duration_seconds: 0, + hours_spent: null, + submit_to: '', + description: '', + submit_date: '', + change_reason: '', +}) + +function openEdit(row) { + editForm._id = row.id + editForm._project_name = row.project_name + editForm.project_phase = row.project_phase + editForm.work_type = row.work_type + editForm.content_type = row.content_type + editForm.duration_minutes = row.duration_minutes || 0 + editForm.duration_seconds = row.duration_seconds || 0 + editForm.hours_spent = row.hours_spent + editForm.submit_to = row.submit_to + editForm.description = row.description || '' + editForm.submit_date = row.submit_date + editForm.change_reason = '' + showEdit.value = true +} + +async function handleUpdate() { + if (!editForm.change_reason?.trim()) { + ElMessage.warning('请填写修改原因') + return + } + editing.value = true + try { + const { _id, _project_name, ...payload } = editForm + await submissionApi.update(_id, payload) + ElMessage.success('修改成功') + showEdit.value = false + load() + } catch { + // 错误已由 axios 拦截器处理 + } finally { + editing.value = false + } +} + onMounted(async () => { load() try { projects.value = await projectApi.list({}) } catch {} diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue index 7f17982..bd74df5 100644 --- a/frontend/src/views/Users.vue +++ b/frontend/src/views/Users.vue @@ -2,7 +2,7 @@
@@ -18,27 +18,27 @@ {{ row.role_name }} - + - + - + - + - + - + @@ -92,8 +92,12 @@