diff --git a/docs/frontend-api-integration.md b/docs/frontend-api-integration.md index f114e1d..87389a0 100644 --- a/docs/frontend-api-integration.md +++ b/docs/frontend-api-integration.md @@ -29,8 +29,10 @@ - 患者列表权限: - `SYSTEM_ADMIN` 查询时必须传 `hospitalId` - 用户管理接口: - - `SYSTEM_ADMIN/HOSPITAL_ADMIN` 可访问列表与创建 - - 删除和工程师绑定医院仅 `SYSTEM_ADMIN` + - `SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR` 可访问列表与创建 + - `DIRECTOR` 页面语义调整为“医生管理”,仅管理本科室医生 + - 工程师绑定医院仅 `SYSTEM_ADMIN` + - 删除:`SYSTEM_ADMIN` 可删除任意无关联用户;`DIRECTOR` 可删除本科室无关联医生 ## 3.1 结构图页面交互调整 @@ -40,9 +42,11 @@ ## 3.2 后台页面路由权限(与后端 RBAC 对齐) - `organization/tree`、`organization/departments`、`organization/groups`、`users` - - `organization/tree`、`organization/departments`、`organization/groups`: + - `organization/tree`、`organization/groups`: `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问 -- `users`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 + - `organization/departments`: + 仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 +- `users`:`SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR` 可访问 - `devices`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 - `organization/hospitals` - 仅 `SYSTEM_ADMIN` 可访问 @@ -56,12 +60,14 @@ ## 3.3 主任/组长组织管理范围 - `DIRECTOR` - - 可查看组织架构、科室列表、小组列表(限定本科室范围) - - 可编辑本科室名称、创建/编辑/删除本科室下小组 + - 可查看组织架构、小组列表(限定本科室范围) + - 可创建/编辑/删除本科室下小组 + - 可进入“医生管理”页,创建/维护本科室医生 - `LEADER` - - 可查看组织架构、科室列表、小组列表(限定本科室/本小组范围) - - 可编辑本科室名称与本小组名称 -- 负责人设置(设主任/设组长)与人员管理入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。 + - 可查看组织架构、小组列表(限定本科室/本小组范围) + - 可编辑本小组名称 +- 主任/组长不再显示独立“科室管理”页面。 +- 负责人设置(设主任/设组长)入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。 ## 4. 本地运行 diff --git a/docs/users.md b/docs/users.md index dd331ee..83b17cc 100644 --- a/docs/users.md +++ b/docs/users.md @@ -19,6 +19,7 @@ - 医院内数据按 `hospitalId` 强隔离。 - 仅 `SYSTEM_ADMIN` 可执行工程师绑定医院。 - `DIRECTOR/LEADER` 可读取用户列表,但仅返回当前科室可见用户。 +- `DIRECTOR` 可创建、查看、编辑、删除本科室医生,但不能跨科室操作,也不能把医生改成其他角色。 - 用户组织字段校验: - 院管/医生/工程师等需有医院归属; - 主任/组长需有科室/小组等必要归属; @@ -31,6 +32,13 @@ - `GET /users`、`GET /users/:id`、`PATCH /users/:id`、`DELETE /users/:id` - `POST /b/users/:id/assign-engineer-hospital` +其中主任侧的常用链路为: + +- `POST /users`:创建本科室医生 +- `GET /users/:id`:查看本科室医生详情 +- `PATCH /users/:id`:修改本科室医生信息 +- `DELETE /users/:id`:删除无关联数据的本科室医生 + ## 5. 开发改造建议 - 若增加角色,请同步修改: diff --git a/src/common/messages.ts b/src/common/messages.ts index 45108a3..8bb9c50 100644 --- a/src/common/messages.ts +++ b/src/common/messages.ts @@ -58,6 +58,7 @@ export const MESSAGES = { '检测到多个同手机号账号,请传 hospitalId 指定登录医院', CREATE_FORBIDDEN: '当前角色无权限创建该用户', HOSPITAL_ADMIN_SCOPE_FORBIDDEN: '医院管理员仅可操作本院非管理员账号', + DIRECTOR_SCOPE_FORBIDDEN: '科室主任仅可操作本科室医生账号', }, TASK: { diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index ba9f0be..faf38ef 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -38,7 +38,7 @@ export class UsersController { * 创建用户。 */ @Post() - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) @ApiOperation({ summary: '创建用户' }) create( @CurrentActor() actor: ActorContext, @@ -61,7 +61,7 @@ export class UsersController { * 查询用户详情。 */ @Get(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) @ApiOperation({ summary: '查询用户详情' }) @ApiParam({ name: 'id', description: '用户 ID' }) findOne(@CurrentActor() actor: ActorContext, @Param('id') id: string) { @@ -72,7 +72,7 @@ export class UsersController { * 更新用户。 */ @Patch(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) @ApiOperation({ summary: '更新用户' }) @ApiParam({ name: 'id', description: '用户 ID' }) update( @@ -87,7 +87,7 @@ export class UsersController { * 删除用户。 */ @Delete(':id') - @Roles(Role.SYSTEM_ADMIN) + @Roles(Role.SYSTEM_ADMIN, Role.DIRECTOR) @ApiOperation({ summary: '删除用户' }) @ApiParam({ name: 'id', description: '用户 ID' }) remove(@CurrentActor() actor: ActorContext, @Param('id') id: string) { diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 8f21294..594de0c 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -278,6 +278,7 @@ export class UsersService { this.assertUpdateTargetRoleAllowed(actor, nextRole); this.assertUpdateHospitalScopeAllowed(actor, nextHospitalId); + this.assertUpdateDepartmentScopeAllowed(actor, nextDepartmentId); const assigningDepartmentOrGroup = (updateUserDto.departmentId !== undefined && nextDepartmentId != null) || @@ -681,7 +682,36 @@ export class UsersService { } if (actor.role !== Role.HOSPITAL_ADMIN) { - throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); + if (actor.role !== Role.DIRECTOR) { + throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); + } + + // 科室主任仅允许创建本科室医生。 + if (targetRole !== Role.DOCTOR) { + throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); + } + + const actorHospitalId = this.requireActorScopeInt( + actor.hospitalId, + MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, + ); + const actorDepartmentId = this.requireActorScopeInt( + actor.departmentId, + MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, + ); + + if (hospitalId != null && hospitalId !== actorHospitalId) { + throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); + } + if (departmentId != null && departmentId !== actorDepartmentId) { + throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); + } + + return { + hospitalId: actorHospitalId, + departmentId: actorDepartmentId, + groupId, + }; } if ( @@ -710,7 +740,7 @@ export class UsersService { } /** - * 读取权限:系统管理员可读全量;其余仅可读自己与本院(医院管理员)。 + * 读取权限:系统管理员可读全量;院管可读本院;主任可读本科室医生。 */ private assertUserReadable( actor: ActorContext, @@ -718,6 +748,7 @@ export class UsersService { id: number; role: Role; hospitalId: number | null; + departmentId: number | null; }, ) { if (actor.role === Role.SYSTEM_ADMIN) { @@ -737,11 +768,31 @@ export class UsersService { } } + if (actor.role === Role.DIRECTOR) { + const actorHospitalId = this.requireActorScopeInt( + actor.hospitalId, + MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, + ); + const actorDepartmentId = this.requireActorScopeInt( + actor.departmentId, + MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, + ); + if ( + target.role === Role.DOCTOR && + target.hospitalId === actorHospitalId && + target.departmentId === actorDepartmentId + ) { + return; + } + + throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + } + throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } /** - * 写权限:医院管理员仅可写本院非管理员账号。 + * 写权限:院管可写本院非管理员账号;主任仅可写本科室医生。 */ private assertUserWritable( actor: ActorContext, @@ -749,12 +800,32 @@ export class UsersService { id: number; role: Role; hospitalId: number | null; + departmentId: number | null; }, ) { if (actor.role === Role.SYSTEM_ADMIN) { return; } + if (actor.role === Role.DIRECTOR) { + const actorHospitalId = this.requireActorScopeInt( + actor.hospitalId, + MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, + ); + const actorDepartmentId = this.requireActorScopeInt( + actor.departmentId, + MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, + ); + if ( + target.role !== Role.DOCTOR || + target.hospitalId !== actorHospitalId || + target.departmentId !== actorDepartmentId + ) { + throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + } + return; + } + if (actor.role !== Role.HOSPITAL_ADMIN) { throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } @@ -786,6 +857,10 @@ export class UsersService { return; } + if (actor.role === Role.DIRECTOR && nextRole !== Role.DOCTOR) { + throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + } + if ( actor.role === Role.HOSPITAL_ADMIN && (nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN) @@ -803,6 +878,17 @@ export class UsersService { actor: ActorContext, hospitalId: number | null, ) { + if (actor.role === Role.DIRECTOR) { + const actorHospitalId = this.requireActorScopeInt( + actor.hospitalId, + MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, + ); + if (hospitalId !== actorHospitalId) { + throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + } + return; + } + if (actor.role !== Role.HOSPITAL_ADMIN) { return; } @@ -818,6 +904,26 @@ export class UsersService { } } + /** + * 写入时科室范围校验,避免主任跨科调整医生归属。 + */ + private assertUpdateDepartmentScopeAllowed( + actor: ActorContext, + departmentId: number | null, + ) { + if (actor.role !== Role.DIRECTOR) { + return; + } + + const actorDepartmentId = this.requireActorScopeInt( + actor.departmentId, + MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, + ); + if (departmentId !== actorDepartmentId) { + throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + } + } + /** * 签发访问令牌。 */ diff --git a/test/e2e/specs/users.e2e-spec.ts b/test/e2e/specs/users.e2e-spec.ts index 18dabd2..67e413e 100644 --- a/test/e2e/specs/users.e2e-spec.ts +++ b/test/e2e/specs/users.e2e-spec.ts @@ -65,6 +65,10 @@ describe('UsersController + BUsersController (e2e)', () => { await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]); }); + it('成功:DIRECTOR 可创建本科室医生', async () => { + await createDoctorUser(ctx.tokens[Role.DIRECTOR]); + }); + it('失败:参数校验失败返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .post('/users') @@ -82,14 +86,31 @@ describe('UsersController + BUsersController (e2e)', () => { expectErrorEnvelope(response, 400, 'phone 必须是合法手机号'); }); - it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + it('失败:DIRECTOR 创建非医生角色返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`) + .send({ + name: uniqueSeedValue('主任创建组长'), + phone: uniquePhone(), + password: 'Seed@1234', + role: Role.LEADER, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + }); + + expectErrorEnvelope(response, 403, '当前角色无权限创建该用户'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'POST /users role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 400, [Role.HOSPITAL_ADMIN]: 400, - [Role.DIRECTOR]: 403, + [Role.DIRECTOR]: 400, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, @@ -152,6 +173,15 @@ describe('UsersController + BUsersController (e2e)', () => { expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId); }); + it('成功:DIRECTOR 可查询本科室医生详情', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId); + }); + it('失败:查询不存在用户返回 404', async () => { const response = await request(ctx.app.getHttpServer()) .get('/users/99999999') @@ -160,14 +190,22 @@ describe('UsersController + BUsersController (e2e)', () => { expectErrorEnvelope(response, 404, '用户不存在'); }); - it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + it('失败:DIRECTOR 查询非本科室医生返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/users/${ctx.fixtures.users.doctorA3Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`); + + expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可访问,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'GET /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 200, [Role.HOSPITAL_ADMIN]: 200, - [Role.DIRECTOR]: 403, + [Role.DIRECTOR]: 200, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, @@ -198,6 +236,19 @@ describe('UsersController + BUsersController (e2e)', () => { expect(response.body.data.name).toBe(nextName); }); + it('成功:DIRECTOR 可更新本科室医生姓名', async () => { + const created = await createDoctorUser(ctx.tokens[Role.DIRECTOR]); + const nextName = uniqueSeedValue('主任更新医生名'); + + const response = await request(ctx.app.getHttpServer()) + .patch(`/users/${created.id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`) + .send({ name: nextName }); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.name).toBe(nextName); + }); + it('失败:非医生调整科室/小组返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .patch(`/users/${ctx.fixtures.users.engineerAId}`) @@ -214,14 +265,32 @@ describe('UsersController + BUsersController (e2e)', () => { ); }); - it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + it('失败:DIRECTOR 不能把医生改成其他角色', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`) + .send({ role: Role.LEADER }); + + expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号'); + }); + + it('失败:DIRECTOR 不能把医生调整到其他科室', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`) + .send({ departmentId: ctx.fixtures.departmentA2Id }); + + expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'PATCH /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 404, [Role.HOSPITAL_ADMIN]: 404, - [Role.DIRECTOR]: 403, + [Role.DIRECTOR]: 404, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, @@ -250,6 +319,16 @@ describe('UsersController + BUsersController (e2e)', () => { expect(response.body.data.id).toBe(created.id); }); + it('成功:DIRECTOR 可删除本科室医生', async () => { + const created = await createDoctorUser(ctx.tokens[Role.DIRECTOR]); + const response = await request(ctx.app.getHttpServer()) + .delete(`/users/${created.id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(created.id); + }); + it('失败:存在关联患者/任务时返回 409', async () => { const response = await request(ctx.app.getHttpServer()) .delete(`/users/${ctx.fixtures.users.doctorAId}`) @@ -258,6 +337,14 @@ describe('UsersController + BUsersController (e2e)', () => { expectErrorEnvelope(response, 409, '用户存在关联患者或任务,无法删除'); }); + it('失败:DIRECTOR 删除非本科室医生返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .delete(`/users/${ctx.fixtures.users.doctorA3Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`); + + expectErrorEnvelope(response, 403, '科室主任仅可操作本科室医生账号'); + }); + it('失败:HOSPITAL_ADMIN 无法删除返回 403', async () => { const response = await request(ctx.app.getHttpServer()) .delete(`/users/${ctx.fixtures.users.doctorAId}`) @@ -266,14 +353,14 @@ describe('UsersController + BUsersController (e2e)', () => { expectErrorEnvelope(response, 403, '无权限执行当前操作'); }); - it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + it('角色矩阵:SYSTEM_ADMIN/DIRECTOR 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'DELETE /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 404, [Role.HOSPITAL_ADMIN]: 403, - [Role.DIRECTOR]: 403, + [Role.DIRECTOR]: 404, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, diff --git a/tyt-admin/src/constants/role-permissions.js b/tyt-admin/src/constants/role-permissions.js index 650febb..2617062 100644 --- a/tyt-admin/src/constants/role-permissions.js +++ b/tyt-admin/src/constants/role-permissions.js @@ -8,6 +8,11 @@ const ORG_MANAGER_ROLES = Object.freeze([ 'DIRECTOR', 'LEADER', ]); +const USER_MANAGER_ROLES = Object.freeze([ + 'SYSTEM_ADMIN', + 'HOSPITAL_ADMIN', + 'DIRECTOR', +]); const TASK_ROLES = Object.freeze(['DOCTOR', 'DIRECTOR', 'LEADER', 'ENGINEER']); const PATIENT_ROLES = Object.freeze([ 'SYSTEM_ADMIN', @@ -21,9 +26,10 @@ const PATIENT_ROLES = Object.freeze([ export const ROLE_PERMISSIONS = Object.freeze({ ORG_TREE: ORG_MANAGER_ROLES, ORG_HOSPITALS: Object.freeze(['SYSTEM_ADMIN']), - ORG_DEPARTMENTS: ORG_MANAGER_ROLES, + // 主任/组长仍可通过接口读取科室信息,但不再开放独立“科室管理”页面。 + ORG_DEPARTMENTS: ADMIN_ROLES, ORG_GROUPS: ORG_MANAGER_ROLES, - USERS: ADMIN_ROLES, + USERS: USER_MANAGER_ROLES, DEVICES: ADMIN_ROLES, TASKS: TASK_ROLES, PATIENTS: PATIENT_ROLES, diff --git a/tyt-admin/src/layouts/AdminLayout.vue b/tyt-admin/src/layouts/AdminLayout.vue index c3a3e00..a015b92 100644 --- a/tyt-admin/src/layouts/AdminLayout.vue +++ b/tyt-admin/src/layouts/AdminLayout.vue @@ -36,7 +36,7 @@ 组织架构图 - + 科室管理 @@ -48,7 +48,7 @@ - 用户管理 + {{ usersMenuLabel }} @@ -129,6 +129,7 @@ const activeMenu = computed(() => { return route.path; }); +const isDirector = computed(() => userStore.role === 'DIRECTOR'); const canAccessUsers = computed(() => hasRolePermission(userStore.role, ROLE_PERMISSIONS.USERS), ); @@ -138,12 +139,18 @@ const canAccessDevices = computed(() => const canAccessOrgTree = computed(() => hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_TREE), ); +const canAccessDepartments = computed(() => + hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_DEPARTMENTS), +); const canAccessTasks = computed(() => hasRolePermission(userStore.role, ROLE_PERMISSIONS.TASKS), ); const canAccessPatients = computed(() => hasRolePermission(userStore.role, ROLE_PERMISSIONS.PATIENTS), ); +const usersMenuLabel = computed(() => + isDirector.value ? '医生管理' : '用户管理', +); const handleCommand = (command) => { if (command === 'logout') { diff --git a/tyt-admin/src/views/Dashboard.vue b/tyt-admin/src/views/Dashboard.vue index c8eef01..117e6da 100644 --- a/tyt-admin/src/views/Dashboard.vue +++ b/tyt-admin/src/views/Dashboard.vue @@ -5,17 +5,13 @@

当前角色:{{ userStore.role || '未登录' }}

- + - +
{{ item.title }}
{{ item.value }}
@@ -75,19 +77,44 @@ const isSystemAdmin = computed(() => userStore.role === 'SYSTEM_ADMIN'); const canViewOrg = computed(() => ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role), ); +const canViewUsers = computed(() => + ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role), +); const canViewPatients = computed(() => ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER', 'DOCTOR'].includes( userStore.role, ), ); -const statCards = computed(() => [ - { key: 'hospitals', title: '医院总数', value: stats.value.hospitals }, - { key: 'departments', title: '科室总数', value: stats.value.departments }, - { key: 'groups', title: '小组总数', value: stats.value.groups }, - { key: 'users', title: '用户总数', value: stats.value.users }, - { key: 'patients', title: '可见患者数', value: stats.value.patients }, -]); +const statCards = computed(() => { + const cards = []; + + if (canViewOrg.value) { + cards.push( + { key: 'hospitals', title: '医院总数', value: stats.value.hospitals }, + { key: 'departments', title: '科室总数', value: stats.value.departments }, + { key: 'groups', title: '小组总数', value: stats.value.groups }, + ); + } + + if (canViewUsers.value) { + cards.push({ + key: 'users', + title: userStore.role === 'DIRECTOR' ? '本科室医生数' : '用户总数', + value: stats.value.users, + }); + } + + if (canViewPatients.value) { + cards.push({ + key: 'patients', + title: '可见患者数', + value: stats.value.patients, + }); + } + + return cards; +}); const fetchHospitalsForFilter = async () => { if (!isSystemAdmin.value) { @@ -104,16 +131,23 @@ const fetchDashboardData = async () => { loading.value = true; try { if (canViewOrg.value) { - const [hospitalRes, departmentRes, groupRes, usersRes] = await Promise.all([ + const [hospitalRes, departmentRes, groupRes] = await Promise.all([ getHospitals({ page: 1, pageSize: 1 }), getDepartments({ page: 1, pageSize: 1 }), getGroups({ page: 1, pageSize: 1 }), - getUsers({ page: 1, pageSize: 1 }), ]); stats.value.hospitals = hospitalRes.total ?? 0; stats.value.departments = departmentRes.total ?? 0; stats.value.groups = groupRes.total ?? 0; + } + + if (canViewUsers.value) { + const usersRes = await getUsers({ + page: 1, + pageSize: 1, + role: userStore.role === 'DIRECTOR' ? 'DOCTOR' : undefined, + }); stats.value.users = usersRes.total ?? 0; } @@ -126,7 +160,9 @@ const fetchDashboardData = async () => { stats.value.patients = 0; } else { const patientRes = await getPatients(params); - stats.value.patients = Array.isArray(patientRes) ? patientRes.length : 0; + stats.value.patients = Array.isArray(patientRes) + ? patientRes.length + : 0; } } } finally { diff --git a/tyt-admin/src/views/users/Users.vue b/tyt-admin/src/views/users/Users.vue index 876bdb8..ee98bc1 100644 --- a/tyt-admin/src/views/users/Users.vue +++ b/tyt-admin/src/views/users/Users.vue @@ -10,18 +10,18 @@ clearable />
- + - - - - - - + @@ -30,7 +30,7 @@ 重置 - 新增用户 + {{ createButtonText }}
@@ -84,7 +84,7 @@ 编辑 + - - - - - - + @@ -150,6 +156,7 @@ v-model="form.hospitalId" placeholder="请选择医院" style="width: 100%" + :disabled="lockHospital" > userStore.role === 'DIRECTOR'); const searchForm = reactive({ keyword: '', - role: '', + role: isDirector.value ? 'DOCTOR' : '', }); const dialogVisible = ref(false); @@ -306,11 +324,24 @@ const assignDialogVisible = ref(false); const currentAssignUser = ref(null); const assignHospitalId = ref(null); +const createButtonText = computed(() => + isDirector.value ? '新增医生' : '新增用户', +); +const dialogTitle = computed(() => { + if (isDirector.value) { + return isEdit.value ? '编辑医生' : '新增医生'; + } + return isEdit.value ? '编辑用户' : '新增用户'; +}); const needHospital = computed(() => form.role && form.role !== 'SYSTEM_ADMIN'); const needDepartment = computed(() => ['DIRECTOR', 'LEADER', 'DOCTOR'].includes(form.role), ); const needGroup = computed(() => ['LEADER', 'DOCTOR'].includes(form.role)); +const lockHospital = computed(() => + ['HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role), +); +const lockDepartment = computed(() => isDirector.value); const rules = computed(() => ({ name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], @@ -379,6 +410,16 @@ const resolveGroupName = (id) => { return groupMap.value.get(id) || `#${id}`; }; +const resolveDirectorScope = () => { + const hospitalId = userStore.userInfo?.hospitalId || null; + const departmentId = userStore.userInfo?.departmentId || null; + if (!hospitalId || !departmentId) { + ElMessage.error('当前主任账号缺少医院或科室归属'); + return null; + } + return { hospitalId, departmentId }; +}; + const fetchCommonData = async () => { const [hospitalRes, departmentRes, groupRes] = await Promise.all([ getHospitals({ page: 1, pageSize: 100 }), @@ -427,7 +468,7 @@ const fetchData = async () => { const res = await getUsers({ page: page.value, pageSize: pageSize.value, - role: searchForm.role || undefined, + role: (isDirector.value ? 'DOCTOR' : searchForm.role) || undefined, keyword: searchForm.keyword || undefined, }); tableData.value = res.list || []; @@ -444,15 +485,29 @@ const handleSearch = () => { const resetSearch = () => { searchForm.keyword = ''; - searchForm.role = ''; + searchForm.role = isDirector.value ? 'DOCTOR' : ''; page.value = 1; fetchData(); }; const openCreateDialog = async () => { + resetForm(); isEdit.value = false; currentId.value = null; - dialogVisible.value = true; + + if (isDirector.value) { + const directorScope = resolveDirectorScope(); + if (!directorScope) { + return; + } + form.role = 'DOCTOR'; + form.hospitalId = directorScope.hospitalId; + await fetchDepartmentsForForm(form.hospitalId); + form.departmentId = directorScope.departmentId; + await fetchGroupsForForm(form.departmentId); + dialogVisible.value = true; + return; + } if (userStore.role === 'HOSPITAL_ADMIN') { form.hospitalId = userStore.userInfo?.hospitalId || null; @@ -460,9 +515,16 @@ const openCreateDialog = async () => { await fetchDepartmentsForForm(form.hospitalId); } } + + dialogVisible.value = true; }; const openEditDialog = async (row) => { + if (isDirector.value && row.role !== 'DOCTOR') { + ElMessage.warning('主任仅可编辑本科室医生'); + return; + } + isEdit.value = true; currentId.value = row.id; @@ -505,6 +567,31 @@ const resetForm = () => { }; const buildSubmitPayload = () => { + if (isDirector.value) { + const directorScope = resolveDirectorScope(); + if (!directorScope) { + return null; + } + + const payload = { + name: form.name, + phone: form.phone, + role: 'DOCTOR', + hospitalId: directorScope.hospitalId, + departmentId: directorScope.departmentId, + groupId: form.groupId, + }; + + if (!isEdit.value) { + return { + ...payload, + password: form.password, + }; + } + + return payload; + } + const payload = { name: form.name, phone: form.phone, @@ -575,6 +662,9 @@ const handleSubmit = async () => { submitLoading.value = true; try { const payload = buildSubmitPayload(); + if (!payload) { + return; + } if (isEdit.value) { await updateUser(currentId.value, payload); ElMessage.success('更新成功'); @@ -592,6 +682,11 @@ const handleSubmit = async () => { }; const handleDelete = (row) => { + if (isDirector.value && row.role !== 'DOCTOR') { + ElMessage.warning('主任仅可删除本科室医生'); + return; + } + ElMessageBox.confirm(`确定要删除用户 "${row.name}" 吗?`, '警告', { confirmButtonText: '确定', cancelButtonText: '取消', @@ -698,16 +793,24 @@ onMounted(async () => { if (route.query.action === 'create') { await openCreateDialog(); - if (route.query.hospitalId) { + if (!isDirector.value && route.query.hospitalId) { form.hospitalId = Number(route.query.hospitalId); await fetchDepartmentsForForm(form.hospitalId); } - if (route.query.departmentId) { + if (!isDirector.value && route.query.departmentId) { form.departmentId = Number(route.query.departmentId); await fetchGroupsForForm(form.departmentId); } } }); + +const canDeleteUser = (row) => { + if (userStore.role === 'SYSTEM_ADMIN') { + return true; + } + + return isDirector.value && row.role === 'DOCTOR'; +};