feat(project): add project management menu grouping #15

Merged
cykj merged 1 commits from codex/issue-13-project-statistics into V2.0 2026-11-04 04:16:38 +00:00
7 changed files with 126 additions and 40 deletions

No files matched your search

+6 -2
View File
@@ -58,8 +58,12 @@ SQL 位于 [docs/sql](../docs/sql/)。当前初始化基线不完整,部分历
[2026-11-02-project_management.sql](../docs/sql/migrations/2026-11-02-project_management.sql),
再依次执行[菜单种子](../docs/sql/migrations/2026-11-02-project_management_menu_seed.sql)与
[项目一级菜单迁移](../docs/sql/migrations/2026-11-02-project_top_menu.sql),
最后执行[全部项目权限种子](../docs/sql/migrations/2026-11-02-project_all_permission.sql)。
“项目”与“市场、生产、质检”平级,包含项目组管理、项目管理、业务统计、历史归属维护。
再执行[全部项目权限种子](../docs/sql/migrations/2026-11-02-project_all_permission.sql),
最后执行[项目管理分组迁移](../docs/sql/migrations/2026-11-04-project_management_group.sql)。
已执行前四个脚本的数据库只需追加分组迁移;完整重跑时也须将分组迁移放在最后。
“项目”与“市场、生产、质检”平级,下设“项目管理”目录,其下包含项目组管理、项目管理、业务统计、历史归属维护。
分组迁移保留四个页面的 ID 和权限,仅为已授权子页面/编辑按钮的角色补齐祖先;
旧 `/project/{页面}` 和 `/market/project-management/{页面}` 地址跳转至 `/project/management/{页面}`。
菜单种子不会给角色新增业务权限,应在现有角色管理分配项目管理权限及祖先菜单;
一级菜单迁移保留已有叶子 ID/授权,仅给已授权项目子菜单的角色补齐新祖先。
“所属项目”默认只列出本人担任负责人或成员的项目组内项目(多组取并集)。
+14 -8
View File
@@ -19,14 +19,20 @@ STATIC_MENU_TREE = [
"isTopMenu": True, "menuCode": "menu:project", "roles": ["admin"],
},
"children": [
{"name": "ProjectManagementTeams", "path": "teams", "component": "/project-management/teams/index",
"meta": {"title": "项目组管理", "keepAlive": True, "menuCode": "project:team:list"}},
{"name": "ProjectManagementProjects", "path": "projects", "component": "/project-management/projects/index",
"meta": {"title": "项目管理", "keepAlive": True, "menuCode": "project:project:list"}},
{"name": "ProjectManagementStatistics", "path": "statistics", "component": "/project-management/statistics/index",
"meta": {"title": "业务统计", "keepAlive": True, "menuCode": "project:statistics:list"}},
{"name": "ProjectManagementAssignments", "path": "assignments", "component": "/project-management/assignments/index",
"meta": {"title": "历史归属维护", "keepAlive": True, "menuCode": "project:assignment:edit"}},
{
"name": "ProjectManagement", "path": "management", "component": "",
"meta": {"title": "项目管理", "keepAlive": False, "menuCode": "menu:project:management"},
"children": [
{"name": "ProjectManagementTeams", "path": "teams", "component": "/project-management/teams/index",
"meta": {"title": "项目组管理", "keepAlive": True, "menuCode": "project:team:list"}},
{"name": "ProjectManagementProjects", "path": "projects", "component": "/project-management/projects/index",
"meta": {"title": "项目管理", "keepAlive": True, "menuCode": "project:project:list"}},
{"name": "ProjectManagementStatistics", "path": "statistics", "component": "/project-management/statistics/index",
"meta": {"title": "业务统计", "keepAlive": True, "menuCode": "project:statistics:list"}},
{"name": "ProjectManagementAssignments", "path": "assignments", "component": "/project-management/assignments/index",
"meta": {"title": "历史归属维护", "keepAlive": True, "menuCode": "project:assignment:edit"}},
],
},
],
},
# ────────────────────────────────────────────
@@ -0,0 +1,46 @@
-- Issue #13:在 project_all_permission 之后执行,可重复执行。
-- 项目 → 项目管理 → 四个原页面。保留页面 ID/权限及移动菜单。
-- 全部项目权限仍挂在项目一级节点,不以目录授权隐式授予业务权限。
START TRANSACTION;
INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, perms,
is_frame, visible, status, tenant_no, order_num, create_dept_id, create_time, update_time, del_flag)
SELECT MD5(CONCAT(top_menu.tenant_no, ':menu:project:management')), '项目管理', top_menu.id,
'M', 'management', '', 'menu:project:management', 1, '1', '1', top_menu.tenant_no,
1, '', NOW(), NOW(), 0
FROM sys_menu top_menu
WHERE top_menu.perms='menu:project' AND top_menu.del_flag=0
AND NOT EXISTS (SELECT 1 FROM sys_menu existing
WHERE existing.tenant_no=top_menu.tenant_no AND existing.perms='menu:project:management');
UPDATE sys_menu leaf
JOIN sys_menu group_menu ON group_menu.tenant_no=leaf.tenant_no
AND group_menu.perms='menu:project:management' AND group_menu.del_flag=0
SET leaf.parent_id=group_menu.id, leaf.update_time=NOW()
WHERE leaf.del_flag=0 AND leaf.perms IN (
'project:team:list', 'project:project:list', 'project:statistics:list', 'project:assignment:edit');
-- 仅已有页面/编辑按钮授权的角色补齐目录、项目一级和 Web 根。
-- 只拥有全部项目选择权限的角色不增加四个管理页面的访问权限。
INSERT INTO sys_role_menu (id, role_id, menu_id, tenant_no, create_dept_id, create_time, update_time, del_flag)
SELECT DISTINCT MD5(CONCAT(grant_row.tenant_no, ':project-management-ancestor:', grant_row.role_id, ':', ancestor.id)),
grant_row.role_id, ancestor.id, grant_row.tenant_no, '', NOW(), NOW(), 0
FROM sys_role_menu grant_row
JOIN sys_role role_row ON role_row.id=grant_row.role_id AND role_row.tenant_no=grant_row.tenant_no
AND role_row.del_flag=0
JOIN sys_menu leaf ON leaf.id=grant_row.menu_id AND leaf.tenant_no=grant_row.tenant_no
AND leaf.del_flag=0 AND leaf.perms IN ('project:team:list', 'project:project:list',
'project:statistics:list', 'project:assignment:edit', 'project:team:edit', 'project:project:edit')
JOIN sys_menu group_menu ON group_menu.tenant_no=grant_row.tenant_no
AND group_menu.perms='menu:project:management' AND group_menu.del_flag=0
JOIN sys_menu top_menu ON top_menu.id=group_menu.parent_id AND top_menu.tenant_no=grant_row.tenant_no
AND top_menu.perms='menu:project' AND top_menu.del_flag=0
JOIN sys_menu ancestor ON ancestor.tenant_no=grant_row.tenant_no AND ancestor.del_flag=0
AND ancestor.id IN (group_menu.id, top_menu.id, top_menu.parent_id)
WHERE grant_row.del_flag=0
AND NOT EXISTS (SELECT 1 FROM sys_role_menu existing
WHERE existing.tenant_no=grant_row.tenant_no AND existing.role_id=grant_row.role_id
AND existing.menu_id=ancestor.id AND existing.del_flag=0)
ON DUPLICATE KEY UPDATE del_flag=0, update_time=NOW();
COMMIT;
@@ -1,5 +1,14 @@
## ADDED Requirements
### Requirement: 项目管理三级导航
Web SHALL 使用“项目 → 项目管理 → 项目组管理 / 项目管理 / 业务统计 / 历史归属维护”层级,复用现有导航组件;原页面权限、名称和功能保持。
#### Scenario: 现有角色升级
- **WHEN** 执行项目管理分组迁移
- **THEN** 保留子页面 ID 与原授权,仅为已授权子页面/编辑按钮的角色补齐目录祖先;全部项目选择权限仍独立挂在项目一级下,不授予管理页面权限。
#### Scenario: 旧地址访问
- **WHEN** 访问 `/project/{页面}` 或 `/market/project-management/{页面}`
- **THEN** 携带原查询参数及 hash 跳转至 `/project/management/{页面}`,目标仍按原页面权限检查。
### Requirement: 组织信息与业务地点独立
系统 SHALL 维护项目组负责人、成员及区域医院资料,区域医院仅为信息记录。
#### Scenario: 修改负责医院
@@ -4,6 +4,17 @@
**架构:** 新管理模块统一校验与统计;现有业务显式接入;前端复用已有样式。
**技术栈:** FastAPI/SQLAlchemy/Pydantic、Vue3/Element Plus、Vue3/Vant。
## 2026-11-04 菜单增加项目管理分组
用户明确指定层级:项目 → 项目管理 → 项目组管理 / 项目管理 / 业务统计 / 历史归属维护。仅重组现有菜单配置,复用市场三级导航,不新增业务页面或修改页面样式。沿用用户自测及先合入 V2.0 再测试的安排,工单保持开放。
- [x] 同步 Web `sqxp.ts` 与后端 `menu.py`,新增 `menu:project:management` 目录,保留四个页面名称、组件和权限码。
- [x] `staticRoutes.ts` 兼容两代旧地址,保留 query 与 hash。
- [x] 新增 `2026-11-04-project_management_group.sql`,保留叶子 ID 与角色授权,仅补新祖先;全部项目权限仍独立挂在项目一级下,更新 README 顺序。
- [x] 独立源码审查未发现 P1/P2,Git 差异核对通过;用户测试模式未运行测试或构建。
- [ ] 提交续接 PR、合入 V2.0、同步主目录;备份并更新已确认测试库菜单,交付用户验收。
以下为此前阶段历史记录;其中待合并等状态已由 2026-11-04 用户明确先合并再测试的指令取代。
用户追加所属项目权限要求:将跨组选择从归属维护权限分离为 `project:project:all`(全部项目),默认仅项目组负责人/有效成员所在组内项目;Web与移动共用后端范围校验。
- [x] 在 `backend/tests/test_project_selection_permissions.py` 增加成员/多组/无组、独立全部项目权限、多角色及职位继承、撤权、跨租户和越权提交用例,先执行确认失败。
- [x] 在 `backend/app/services/project_management.py` 统一候选范围,赋值校验改用独立权限;筛选仍叠加原订单数据范围,历史回显与转换继承保持原有规则。
+30 -22
View File
@@ -20,28 +20,36 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
},
children: [
{
name: 'ProjectManagementTeams',
path: 'teams',
component: '/project-management/teams/index',
meta: { title: '项目组管理', keepAlive: true, menuCode: 'project:team:list' }
},
{
name: 'ProjectManagementProjects',
path: 'projects',
component: '/project-management/projects/index',
meta: { title: '项目管理', keepAlive: true, menuCode: 'project:project:list' }
},
{
name: 'ProjectManagementStatistics',
path: 'statistics',
component: '/project-management/statistics/index',
meta: { title: '业务统计', keepAlive: true, menuCode: 'project:statistics:list' }
},
{
name: 'ProjectManagementAssignments',
path: 'assignments',
component: '/project-management/assignments/index',
meta: { title: '历史归属维护', keepAlive: true, menuCode: 'project:assignment:edit' }
name: 'ProjectManagement',
path: 'management',
component: '',
meta: { title: '项目管理', keepAlive: false, menuCode: 'menu:project:management' },
children: [
{
name: 'ProjectManagementTeams',
path: 'teams',
component: '/project-management/teams/index',
meta: { title: '项目组管理', keepAlive: true, menuCode: 'project:team:list' }
},
{
name: 'ProjectManagementProjects',
path: 'projects',
component: '/project-management/projects/index',
meta: { title: '项目管理', keepAlive: true, menuCode: 'project:project:list' }
},
{
name: 'ProjectManagementStatistics',
path: 'statistics',
component: '/project-management/statistics/index',
meta: { title: '业务统计', keepAlive: true, menuCode: 'project:statistics:list' }
},
{
name: 'ProjectManagementAssignments',
path: 'assignments',
component: '/project-management/assignments/index',
meta: { title: '历史归属维护', keepAlive: true, menuCode: 'project:assignment:edit' }
}
]
}
]
},
+10 -8
View File
@@ -11,14 +11,16 @@ import { AppRouteRecordRaw } from '@/utils/router'
* 2、静态路由不管是否登录都可以访问
*/
export const staticRoutes: AppRouteRecordRaw[] = [
...['teams', 'projects', 'statistics', 'assignments'].map((section) => ({
path: `/market/project-management/${section}`,
redirect: (to: { query: Record<string, any>; hash: string }) => ({
path: `/project/${section}`,
query: to.query,
hash: to.hash
})
})),
...['/market/project-management', '/project'].flatMap((prefix) =>
['teams', 'projects', 'statistics', 'assignments'].map((section) => ({
path: `${prefix}/${section}`,
redirect: (to: { query: Record<string, any>; hash: string }) => ({
path: `/project/management/${section}`,
query: to.query,
hash: to.hash
})
}))
),
{
path: '/auth/login',
name: 'Login',