diff --git a/docs/frontend-api-integration.md b/docs/frontend-api-integration.md
index 14481e4..0626cc5 100644
--- a/docs/frontend-api-integration.md
+++ b/docs/frontend-api-integration.md
@@ -19,7 +19,7 @@
## 3. 角色权限提示
- 任务接口权限:
- - `DOCTOR`:发布、取消
+ - `DOCTOR/DIRECTOR/LEADER`:发布、取消(仅可取消自己创建的任务)
- `ENGINEER`:接收、完成
- 患者列表权限:
- `SYSTEM_ADMIN` 查询时必须传 `hospitalId`
@@ -27,6 +27,36 @@
- `SYSTEM_ADMIN/HOSPITAL_ADMIN` 可访问列表与创建
- 删除和工程师绑定医院仅 `SYSTEM_ADMIN`
+## 3.1 结构图页面交互调整
+
+- 医院管理员视角下,右侧下级列表会优先显示“人员”节点,再显示组织节点。
+- 选中人员节点时,右侧展示人员详情(角色、手机号、所属医院/科室/小组),不再显示空白占位。
+
+## 3.2 后台页面路由权限(与后端 RBAC 对齐)
+
+- `organization/tree`、`organization/departments`、`organization/groups`、`users`
+ - `organization/tree`、`organization/departments`、`organization/groups`:
+ `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问
+ - `users`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问
+- `organization/hospitals`
+ - 仅 `SYSTEM_ADMIN` 可访问
+- `tasks`
+ - 仅 `DOCTOR`、`DIRECTOR`、`LEADER`、`ENGINEER` 可访问
+- `patients`
+ - 仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问
+
+前端已在路由守卫和侧边栏菜单同时做权限控制,无权限角色会被拦截并跳转到首页,避免进入页面后触发接口 `403`。
+
+## 3.3 主任/组长组织管理范围
+
+- `DIRECTOR`
+ - 可查看组织架构、科室列表、小组列表(限定本科室范围)
+ - 可编辑本科室名称、创建/编辑/删除本科室下小组
+- `LEADER`
+ - 可查看组织架构、科室列表、小组列表(限定本科室/本小组范围)
+ - 可编辑本科室名称与本小组名称
+- 负责人设置(设主任/设组长)与人员管理入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。
+
## 4. 本地运行
在 `tyt-admin` 目录执行:
diff --git a/docs/patients.md b/docs/patients.md
index 6371770..bfdc3a9 100644
--- a/docs/patients.md
+++ b/docs/patients.md
@@ -16,7 +16,7 @@
## 2.1 B 端 CRUD
- `GET /b/patients`:按角色查询可见患者
-- `GET /b/patients/doctors`:查询当前角色可见的医生候选(用于患者表单)
+- `GET /b/patients/doctors`:查询当前角色可见的归属人员候选(医生/主任/组长,用于患者表单)
- `POST /b/patients`:创建患者
- `GET /b/patients/:id`:查询患者详情
- `PATCH /b/patients/:id`:更新患者
diff --git a/docs/tasks.md b/docs/tasks.md
index 7139888..3079186 100644
--- a/docs/tasks.md
+++ b/docs/tasks.md
@@ -14,7 +14,7 @@
## 3. 角色权限
-- 医生:发布任务、取消自己创建的任务
+- 医生/主任/组长:发布任务、取消自己创建的任务
- 工程师:接收任务、完成自己接收的任务
- 其他角色:默认拒绝
diff --git a/docs/users.md b/docs/users.md
index 09b343e..dd331ee 100644
--- a/docs/users.md
+++ b/docs/users.md
@@ -18,6 +18,7 @@
- 医院内数据按 `hospitalId` 强隔离。
- 仅 `SYSTEM_ADMIN` 可执行工程师绑定医院。
+- `DIRECTOR/LEADER` 可读取用户列表,但仅返回当前科室可见用户。
- 用户组织字段校验:
- 院管/医生/工程师等需有医院归属;
- 主任/组长需有科室/小组等必要归属;
diff --git a/src/common/messages.ts b/src/common/messages.ts
index 7e5af34..de81d44 100644
--- a/src/common/messages.ts
+++ b/src/common/messages.ts
@@ -65,7 +65,7 @@ export const MESSAGES = {
CANCEL_ONLY_PENDING_ACCEPTED: '仅待接收/已接收任务可取消',
ENGINEER_ALREADY_ASSIGNED: '任务已被其他工程师接收',
ENGINEER_ONLY_ASSIGNEE: '仅任务接收工程师可完成任务',
- CANCEL_ONLY_CREATOR: '仅任务创建医生可取消任务',
+ CANCEL_ONLY_CREATOR: '仅任务创建者可取消任务',
ACTOR_ROLE_FORBIDDEN: '当前角色无权限执行该任务操作',
ACTOR_HOSPITAL_REQUIRED: '当前登录上下文缺少医院信息',
},
@@ -75,9 +75,9 @@ export const MESSAGES = {
ROLE_FORBIDDEN: '当前角色无权限查询患者列表',
GROUP_REQUIRED: '组长查询需携带 groupId',
DEPARTMENT_REQUIRED: '主任查询需携带 departmentId',
- DOCTOR_NOT_FOUND: '归属医生不存在',
- DOCTOR_ROLE_REQUIRED: '归属用户必须为医生角色',
- DOCTOR_SCOPE_FORBIDDEN: '仅可选择当前权限范围内医生',
+ DOCTOR_NOT_FOUND: '归属人员不存在',
+ DOCTOR_ROLE_REQUIRED: '归属用户必须为医生/主任/组长角色',
+ DOCTOR_SCOPE_FORBIDDEN: '仅可选择当前权限范围内医生/主任/组长',
DELETE_CONFLICT: '患者存在关联设备,无法删除',
PHONE_IDCARD_REQUIRED: 'phone 与 idCardHash 均为必填',
LIFE_CYCLE_NOT_FOUND: '未找到匹配的患者档案,请先确认手机号与身份证哈希',
@@ -90,6 +90,9 @@ export const MESSAGES = {
DEPARTMENT_NOT_FOUND: '科室不存在',
GROUP_NOT_FOUND: '小组不存在',
HOSPITAL_ADMIN_SCOPE_INVALID: '院管仅可操作本院组织数据',
+ ACTOR_HOSPITAL_REQUIRED: '当前登录上下文缺少医院信息',
+ ACTOR_DEPARTMENT_REQUIRED: '当前登录上下文缺少科室信息',
+ ACTOR_GROUP_REQUIRED: '当前登录上下文缺少小组信息',
SYSTEM_ADMIN_ONLY_CREATE_HOSPITAL: '仅系统管理员可创建医院',
SYSTEM_ADMIN_ONLY_DELETE_HOSPITAL: '仅系统管理员可删除医院',
HOSPITAL_NAME_REQUIRED: '医院名称不能为空',
diff --git a/src/departments/departments.controller.ts b/src/departments/departments.controller.ts
index cbde73a..4e202e7 100644
--- a/src/departments/departments.controller.ts
+++ b/src/departments/departments.controller.ts
@@ -55,7 +55,12 @@ export class DepartmentsController {
* 查询科室列表。
*/
@Get()
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询科室列表' })
@ApiQuery({ name: 'hospitalId', required: false, description: '医院 ID' })
findAll(
@@ -69,7 +74,12 @@ export class DepartmentsController {
* 查询科室详情。
*/
@Get(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询科室详情' })
@ApiParam({ name: 'id', description: '科室 ID' })
findOne(
@@ -83,7 +93,12 @@ export class DepartmentsController {
* 更新科室。
*/
@Patch(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '更新科室' })
update(
@CurrentActor() actor: ActorContext,
diff --git a/src/departments/departments.service.ts b/src/departments/departments.service.ts
index 10dcb05..eb6d40f 100644
--- a/src/departments/departments.service.ts
+++ b/src/departments/departments.service.ts
@@ -1,4 +1,9 @@
-import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
+import {
+ BadRequestException,
+ ForbiddenException,
+ Injectable,
+ NotFoundException,
+} from '@nestjs/common';
import { Prisma } from '../generated/prisma/client.js';
import { Role } from '../generated/prisma/enums.js';
import type { ActorContext } from '../common/actor-context.js';
@@ -37,10 +42,15 @@ export class DepartmentsService {
}
/**
- * 查询科室列表:院管默认限定本院。
+ * 查询科室列表:院管限定本院;主任/组长限定本科室。
*/
async findAll(actor: ActorContext, query: OrganizationQueryDto) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const paging = this.access.resolvePaging(query);
const where: Prisma.DepartmentWhereInput = {};
@@ -48,13 +58,15 @@ export class DepartmentsService {
where.name = { contains: query.keyword.trim(), mode: 'insensitive' };
}
- const targetHospitalId =
- actor.role === Role.HOSPITAL_ADMIN ? actor.hospitalId : query.hospitalId;
- if (targetHospitalId != null) {
- where.hospitalId = this.access.toInt(targetHospitalId, MESSAGES.ORG.HOSPITAL_ID_REQUIRED);
- }
- if (actor.role === Role.HOSPITAL_ADMIN && where.hospitalId == null) {
- throw new BadRequestException(MESSAGES.ORG.HOSPITAL_ID_REQUIRED);
+ if (actor.role === Role.HOSPITAL_ADMIN) {
+ where.hospitalId = this.access.requireActorHospitalId(actor);
+ } else if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) {
+ where.id = this.access.requireActorDepartmentId(actor);
+ } else if (query.hospitalId != null) {
+ where.hospitalId = this.access.toInt(
+ query.hospitalId,
+ MESSAGES.ORG.HOSPITAL_ID_REQUIRED,
+ );
}
const [total, list] = await this.prisma.$transaction([
@@ -72,10 +84,15 @@ export class DepartmentsService {
}
/**
- * 查询科室详情:院管仅可查看本院科室。
+ * 查询科室详情:院管仅可查看本院;主任/组长仅可查看本科室。
*/
async findOne(actor: ActorContext, id: number) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const departmentId = this.access.toInt(id, MESSAGES.ORG.DEPARTMENT_ID_REQUIRED);
const department = await this.prisma.department.findUnique({
where: { id: departmentId },
@@ -87,12 +104,20 @@ export class DepartmentsService {
if (!department) {
throw new NotFoundException(MESSAGES.ORG.DEPARTMENT_NOT_FOUND);
}
- this.access.assertHospitalScope(actor, department.hospitalId);
+ if (actor.role === Role.HOSPITAL_ADMIN) {
+ this.access.assertHospitalScope(actor, department.hospitalId);
+ }
+ if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) {
+ const actorDepartmentId = this.access.requireActorDepartmentId(actor);
+ if (department.id !== actorDepartmentId) {
+ throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
+ }
+ }
return department;
}
/**
- * 更新科室:院管仅可修改本院科室。
+ * 更新科室:院管仅可修改本院;主任/组长仅可修改本科室。
*/
async update(actor: ActorContext, id: number, dto: UpdateDepartmentDto) {
const current = await this.findOne(actor, id);
@@ -116,6 +141,7 @@ export class DepartmentsService {
* 删除科室:院管仅可删本院科室。
*/
async remove(actor: ActorContext, id: number) {
+ this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
const current = await this.findOne(actor, id);
try {
return await this.prisma.department.delete({ where: { id: current.id } });
diff --git a/src/groups/groups.controller.ts b/src/groups/groups.controller.ts
index 70b6f72..551ef7b 100644
--- a/src/groups/groups.controller.ts
+++ b/src/groups/groups.controller.ts
@@ -41,7 +41,7 @@ export class GroupsController {
* 创建小组。
*/
@Post()
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR)
@ApiOperation({ summary: '创建小组' })
create(
@CurrentActor() actor: ActorContext,
@@ -54,7 +54,12 @@ export class GroupsController {
* 查询小组列表。
*/
@Get()
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询小组列表' })
findAll(
@CurrentActor() actor: ActorContext,
@@ -67,7 +72,12 @@ export class GroupsController {
* 查询小组详情。
*/
@Get(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询小组详情' })
@ApiParam({ name: 'id', description: '小组 ID' })
findOne(
@@ -81,7 +91,12 @@ export class GroupsController {
* 更新小组。
*/
@Patch(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '更新小组' })
update(
@CurrentActor() actor: ActorContext,
@@ -95,7 +110,7 @@ export class GroupsController {
* 删除小组。
*/
@Delete(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR)
@ApiOperation({ summary: '删除小组' })
remove(
@CurrentActor() actor: ActorContext,
diff --git a/src/groups/groups.service.ts b/src/groups/groups.service.ts
index 7175600..314e3f1 100644
--- a/src/groups/groups.service.ts
+++ b/src/groups/groups.service.ts
@@ -1,4 +1,9 @@
-import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
+import {
+ BadRequestException,
+ ForbiddenException,
+ Injectable,
+ NotFoundException,
+} from '@nestjs/common';
import { Prisma } from '../generated/prisma/client.js';
import { Role } from '../generated/prisma/enums.js';
import type { ActorContext } from '../common/actor-context.js';
@@ -20,13 +25,25 @@ export class GroupsService {
) {}
/**
- * 创建小组:系统管理员可跨院;院管仅可在本院科室下创建。
+ * 创建小组:系统管理员可跨院;院管仅可在本院;主任仅可在本科室创建。
*/
async create(actor: ActorContext, dto: CreateGroupDto) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ ]);
const departmentId = this.access.toInt(dto.departmentId, MESSAGES.ORG.DEPARTMENT_ID_REQUIRED);
const department = await this.access.ensureDepartmentExists(departmentId);
- this.access.assertHospitalScope(actor, department.hospitalId);
+ if (actor.role === Role.HOSPITAL_ADMIN) {
+ this.access.assertHospitalScope(actor, department.hospitalId);
+ }
+ if (actor.role === Role.DIRECTOR) {
+ const actorDepartmentId = this.access.requireActorDepartmentId(actor);
+ if (actorDepartmentId !== department.id) {
+ throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
+ }
+ }
return this.prisma.group.create({
data: {
@@ -37,10 +54,15 @@ export class GroupsService {
}
/**
- * 查询小组列表:院管默认仅返回本院数据。
+ * 查询小组列表:院管限定本院;主任限定本科室;组长限定本组。
*/
async findAll(actor: ActorContext, query: OrganizationQueryDto) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const paging = this.access.resolvePaging(query);
const where: Prisma.GroupWhereInput = {};
@@ -52,10 +74,11 @@ export class GroupsService {
}
if (actor.role === Role.HOSPITAL_ADMIN) {
- if (!actor.hospitalId) {
- throw new BadRequestException(MESSAGES.ORG.HOSPITAL_ID_REQUIRED);
- }
- where.department = { hospitalId: actor.hospitalId };
+ where.department = { hospitalId: this.access.requireActorHospitalId(actor) };
+ } else if (actor.role === Role.DIRECTOR) {
+ where.departmentId = this.access.requireActorDepartmentId(actor);
+ } else if (actor.role === Role.LEADER) {
+ where.id = this.access.requireActorGroupId(actor);
} else if (query.hospitalId != null) {
where.department = {
hospitalId: this.access.toInt(query.hospitalId, MESSAGES.ORG.HOSPITAL_ID_REQUIRED),
@@ -80,10 +103,15 @@ export class GroupsService {
}
/**
- * 查询小组详情:院管仅可查看本院小组。
+ * 查询小组详情:院管仅可查看本院;主任仅可查看本科室;组长仅可查看本组。
*/
async findOne(actor: ActorContext, id: number) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const groupId = this.access.toInt(id, MESSAGES.ORG.GROUP_ID_REQUIRED);
const group = await this.prisma.group.findUnique({
where: { id: groupId },
@@ -95,12 +123,26 @@ export class GroupsService {
if (!group) {
throw new NotFoundException(MESSAGES.ORG.GROUP_NOT_FOUND);
}
- this.access.assertHospitalScope(actor, group.department.hospital.id);
+ if (actor.role === Role.HOSPITAL_ADMIN) {
+ this.access.assertHospitalScope(actor, group.department.hospital.id);
+ }
+ if (actor.role === Role.DIRECTOR) {
+ const actorDepartmentId = this.access.requireActorDepartmentId(actor);
+ if (group.department.id !== actorDepartmentId) {
+ throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
+ }
+ }
+ if (actor.role === Role.LEADER) {
+ const actorGroupId = this.access.requireActorGroupId(actor);
+ if (group.id !== actorGroupId) {
+ throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
+ }
+ }
return group;
}
/**
- * 更新小组:院管仅可修改本院小组。
+ * 更新小组:院管仅可修改本院;主任仅可修改本科室;组长仅可修改本组。
*/
async update(actor: ActorContext, id: number, dto: UpdateGroupDto) {
const current = await this.findOne(actor, id);
@@ -121,9 +163,14 @@ export class GroupsService {
}
/**
- * 删除小组:院管仅可删除本院小组。
+ * 删除小组:院管仅可删除本院;主任仅可删除本科室小组。
*/
async remove(actor: ActorContext, id: number) {
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ ]);
const current = await this.findOne(actor, id);
try {
return await this.prisma.group.delete({ where: { id: current.id } });
diff --git a/src/hospitals/hospitals.controller.ts b/src/hospitals/hospitals.controller.ts
index 892190c..7ba503d 100644
--- a/src/hospitals/hospitals.controller.ts
+++ b/src/hospitals/hospitals.controller.ts
@@ -54,7 +54,12 @@ export class HospitalsController {
* 查询医院列表(系统管理员全量,院管仅本院)。
*/
@Get()
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询医院列表' })
findAll(
@CurrentActor() actor: ActorContext,
@@ -67,7 +72,12 @@ export class HospitalsController {
* 查询医院详情。
*/
@Get(':id')
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询医院详情' })
@ApiParam({ name: 'id', description: '医院 ID' })
findOne(
diff --git a/src/hospitals/hospitals.service.ts b/src/hospitals/hospitals.service.ts
index a499db3..7344e1c 100644
--- a/src/hospitals/hospitals.service.ts
+++ b/src/hospitals/hospitals.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '../generated/prisma/client.js';
import { Role } from '../generated/prisma/enums.js';
import type { ActorContext } from '../common/actor-context.js';
@@ -32,21 +32,23 @@ export class HospitalsService {
}
/**
- * 查询医院列表:系统管理员可查全量;院管仅可查看本院。
+ * 查询医院列表:系统管理员可查全量;院管/主任/组长仅可查看本院。
*/
async findAll(actor: ActorContext, query: OrganizationQueryDto) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const paging = this.access.resolvePaging(query);
const where: Prisma.HospitalWhereInput = {};
if (query.keyword) {
where.name = { contains: query.keyword.trim(), mode: 'insensitive' };
}
- if (actor.role === Role.HOSPITAL_ADMIN) {
- if (!actor.hospitalId) {
- throw new BadRequestException(MESSAGES.ORG.HOSPITAL_ID_REQUIRED);
- }
- where.id = actor.hospitalId ?? undefined;
+ if (actor.role !== Role.SYSTEM_ADMIN) {
+ where.id = this.access.requireActorHospitalId(actor);
}
const [total, list] = await this.prisma.$transaction([
@@ -63,10 +65,15 @@ export class HospitalsService {
}
/**
- * 查询医院详情:院管仅能查看本院。
+ * 查询医院详情:院管/主任/组长仅能查看本院。
*/
async findOne(actor: ActorContext, id: number) {
- this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
+ this.access.assertRole(actor, [
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ]);
const hospitalId = this.access.toInt(id, MESSAGES.ORG.HOSPITAL_ID_REQUIRED);
const hospital = await this.prisma.hospital.findUnique({
where: { id: hospitalId },
diff --git a/src/organization-common/organization-access.service.ts b/src/organization-common/organization-access.service.ts
index fb584f9..d6b517b 100644
--- a/src/organization-common/organization-access.service.ts
+++ b/src/organization-common/organization-access.service.ts
@@ -39,17 +39,70 @@ export class OrganizationAccessService {
}
/**
- * 校验院管的数据作用域限制。
+ * 校验组织域医院级作用域限制。
*/
assertHospitalScope(actor: ActorContext, targetHospitalId: number) {
- if (actor.role !== Role.HOSPITAL_ADMIN) {
+ if (actor.role === Role.SYSTEM_ADMIN) {
return;
}
- if (!actor.hospitalId || actor.hospitalId !== targetHospitalId) {
+
+ const hospitalScopedRoles: Role[] = [
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ ];
+ if (!hospitalScopedRoles.includes(actor.role)) {
+ return;
+ }
+
+ const actorHospitalId = this.requireActorHospitalId(actor);
+ if (actorHospitalId !== targetHospitalId) {
throw new ForbiddenException(MESSAGES.ORG.HOSPITAL_ADMIN_SCOPE_INVALID);
}
}
+ /**
+ * 读取并校验当前登录上下文的医院 ID。
+ */
+ requireActorHospitalId(actor: ActorContext): number {
+ if (
+ typeof actor.hospitalId !== 'number'
+ || !Number.isInteger(actor.hospitalId)
+ || actor.hospitalId <= 0
+ ) {
+ throw new BadRequestException(MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED);
+ }
+ return actor.hospitalId;
+ }
+
+ /**
+ * 读取并校验当前登录上下文的科室 ID。
+ */
+ requireActorDepartmentId(actor: ActorContext): number {
+ if (
+ typeof actor.departmentId !== 'number'
+ || !Number.isInteger(actor.departmentId)
+ || actor.departmentId <= 0
+ ) {
+ throw new BadRequestException(MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED);
+ }
+ return actor.departmentId;
+ }
+
+ /**
+ * 读取并校验当前登录上下文的小组 ID。
+ */
+ requireActorGroupId(actor: ActorContext): number {
+ if (
+ typeof actor.groupId !== 'number'
+ || !Number.isInteger(actor.groupId)
+ || actor.groupId <= 0
+ ) {
+ throw new BadRequestException(MESSAGES.ORG.ACTOR_GROUP_REQUIRED);
+ }
+ return actor.groupId;
+ }
+
/**
* 分页参数标准化。
*/
diff --git a/src/patients/b-patients/b-patients.controller.ts b/src/patients/b-patients/b-patients.controller.ts
index 2723cdf..346788e 100644
--- a/src/patients/b-patients/b-patients.controller.ts
+++ b/src/patients/b-patients/b-patients.controller.ts
@@ -38,7 +38,7 @@ export class BPatientsController {
constructor(private readonly patientsService: BPatientsService) {}
/**
- * 查询当前角色可选择的医生列表(用于创建/编辑患者)。
+ * 查询当前角色可选择的归属人员列表(医生/主任/组长)。
*/
@Get('doctors')
@Roles(
@@ -48,7 +48,7 @@ export class BPatientsController {
Role.LEADER,
Role.DOCTOR,
)
- @ApiOperation({ summary: '查询当前角色可见医生列表' })
+ @ApiOperation({ summary: '查询当前角色可见归属人员列表' })
@ApiQuery({
name: 'hospitalId',
required: false,
diff --git a/src/patients/b-patients/b-patients.service.ts b/src/patients/b-patients/b-patients.service.ts
index b2e0f7a..26ae325 100644
--- a/src/patients/b-patients/b-patients.service.ts
+++ b/src/patients/b-patients/b-patients.service.ts
@@ -13,6 +13,8 @@ import { MESSAGES } from '../../common/messages.js';
import { CreatePatientDto } from '../dto/create-patient.dto.js';
import { UpdatePatientDto } from '../dto/update-patient.dto.js';
+const PATIENT_OWNER_ROLES: Role[] = [Role.DOCTOR, Role.DIRECTOR, Role.LEADER];
+
/**
* B 端患者服务:承载院内可见性隔离与患者 CRUD。
*/
@@ -39,12 +41,12 @@ export class BPatientsService {
}
/**
- * 查询当前角色可见医生列表,用于患者表单选择。
+ * 查询当前角色可见归属人员列表(医生/主任/组长),用于患者表单选择。
*/
async findVisibleDoctors(actor: ActorContext, requestedHospitalId?: number) {
const hospitalId = this.resolveHospitalId(actor, requestedHospitalId);
const where: Prisma.UserWhereInput = {
- role: Role.DOCTOR,
+ role: { in: PATIENT_OWNER_ROLES },
hospitalId,
};
@@ -274,7 +276,7 @@ export class BPatientsService {
if (!doctor) {
throw new NotFoundException(MESSAGES.PATIENT.DOCTOR_NOT_FOUND);
}
- if (doctor.role !== Role.DOCTOR) {
+ if (!PATIENT_OWNER_ROLES.includes(doctor.role)) {
throw new BadRequestException(MESSAGES.PATIENT.DOCTOR_ROLE_REQUIRED);
}
if (!doctor.hospitalId) {
@@ -324,7 +326,7 @@ export class BPatientsService {
}
where.doctor = {
groupId: actor.groupId,
- role: Role.DOCTOR,
+ role: { in: PATIENT_OWNER_ROLES },
};
break;
case Role.DIRECTOR:
@@ -333,7 +335,7 @@ export class BPatientsService {
}
where.doctor = {
departmentId: actor.departmentId,
- role: Role.DOCTOR,
+ role: { in: PATIENT_OWNER_ROLES },
};
break;
case Role.HOSPITAL_ADMIN:
diff --git a/src/patients/dto/create-patient.dto.ts b/src/patients/dto/create-patient.dto.ts
index f926ffd..70a26ef 100644
--- a/src/patients/dto/create-patient.dto.ts
+++ b/src/patients/dto/create-patient.dto.ts
@@ -27,7 +27,7 @@ export class CreatePatientDto {
@IsString({ message: 'idCardHash 必须是字符串' })
idCardHash!: string;
- @ApiProperty({ description: '归属医生 ID', example: 10001 })
+ @ApiProperty({ description: '归属人员 ID(医生/主任/组长)', example: 10001 })
@Type(() => Number)
@IsInt({ message: 'doctorId 必须是整数' })
@Min(1, { message: 'doctorId 必须大于 0' })
diff --git a/src/tasks/b-tasks/b-tasks.controller.ts b/src/tasks/b-tasks/b-tasks.controller.ts
index 82f61ab..0e7f8a8 100644
--- a/src/tasks/b-tasks/b-tasks.controller.ts
+++ b/src/tasks/b-tasks/b-tasks.controller.ts
@@ -23,11 +23,11 @@ export class BTasksController {
constructor(private readonly taskService: TaskService) {}
/**
- * 医生发布调压任务。
+ * 医生/主任/组长发布调压任务。
*/
@Post('publish')
- @Roles(Role.DOCTOR)
- @ApiOperation({ summary: '发布任务(DOCTOR)' })
+ @Roles(Role.DOCTOR, Role.DIRECTOR, Role.LEADER)
+ @ApiOperation({ summary: '发布任务(DOCTOR/DIRECTOR/LEADER)' })
publish(@CurrentActor() actor: ActorContext, @Body() dto: PublishTaskDto) {
return this.taskService.publishTask(actor, dto);
}
@@ -53,11 +53,11 @@ export class BTasksController {
}
/**
- * 医生取消调压任务。
+ * 医生/主任/组长取消调压任务(仅任务创建者)。
*/
@Post('cancel')
- @Roles(Role.DOCTOR)
- @ApiOperation({ summary: '取消任务(DOCTOR)' })
+ @Roles(Role.DOCTOR, Role.DIRECTOR, Role.LEADER)
+ @ApiOperation({ summary: '取消任务(DOCTOR/DIRECTOR/LEADER)' })
cancel(@CurrentActor() actor: ActorContext, @Body() dto: CancelTaskDto) {
return this.taskService.cancelTask(actor, dto);
}
diff --git a/src/tasks/task.service.ts b/src/tasks/task.service.ts
index 6e8d551..c7aa1b3 100644
--- a/src/tasks/task.service.ts
+++ b/src/tasks/task.service.ts
@@ -26,10 +26,10 @@ export class TaskService {
) {}
/**
- * 发布任务:医生创建主任务与明细,状态初始化为 PENDING。
+ * 发布任务:医生/主任/组长创建主任务与明细,状态初始化为 PENDING。
*/
async publishTask(actor: ActorContext, dto: PublishTaskDto) {
- this.assertRole(actor, [Role.DOCTOR]);
+ this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]);
const hospitalId = this.requireHospitalId(actor);
if (!Array.isArray(dto.items) || dto.items.length === 0) {
@@ -214,10 +214,10 @@ export class TaskService {
}
/**
- * 取消任务:创建医生可将 PENDING/ACCEPTED 任务取消。
+ * 取消任务:任务创建者可将 PENDING/ACCEPTED 任务取消。
*/
async cancelTask(actor: ActorContext, dto: CancelTaskDto) {
- this.assertRole(actor, [Role.DOCTOR]);
+ this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]);
const hospitalId = this.requireHospitalId(actor);
const task = await this.prisma.task.findFirst({
diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts
index d80a13f..834b5ac 100644
--- a/src/users/users.controller.ts
+++ b/src/users/users.controller.ts
@@ -21,6 +21,8 @@ import { Role } from '../generated/prisma/enums.js';
import { UsersService } from './users.service.js';
import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js';
+import { CurrentActor } from '../auth/current-actor.decorator.js';
+import type { ActorContext } from '../common/actor-context.js';
/**
* 用户管理控制器:面向 B 端后台的用户 CRUD。
@@ -46,10 +48,15 @@ export class UsersController {
* 查询用户列表。
*/
@Get()
- @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
+ @Roles(
+ Role.SYSTEM_ADMIN,
+ Role.HOSPITAL_ADMIN,
+ Role.DIRECTOR,
+ Role.LEADER,
+ )
@ApiOperation({ summary: '查询用户列表' })
- findAll() {
- return this.usersService.findAll();
+ findAll(@CurrentActor() actor: ActorContext) {
+ return this.usersService.findAll(actor);
}
/**
diff --git a/src/users/users.service.ts b/src/users/users.service.ts
index 1e9b268..bf46a71 100644
--- a/src/users/users.service.ts
+++ b/src/users/users.service.ts
@@ -177,10 +177,31 @@ export class UsersService {
}
/**
- * 查询用户列表。
+ * 查询用户列表:按角色自动收敛可见范围。
*/
- async findAll() {
+ async findAll(actor: ActorContext) {
+ const where: Prisma.UserWhereInput = {};
+
+ if (actor.role === Role.HOSPITAL_ADMIN) {
+ where.hospitalId = this.requireActorScopeInt(
+ actor.hospitalId,
+ MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
+ );
+ } else if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) {
+ where.hospitalId = this.requireActorScopeInt(
+ actor.hospitalId,
+ MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
+ );
+ where.departmentId = this.requireActorScopeInt(
+ actor.departmentId,
+ MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
+ );
+ } else if (actor.role !== Role.SYSTEM_ADMIN) {
+ throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
+ }
+
return this.prisma.user.findMany({
+ where,
select: SAFE_USER_SELECT,
orderBy: { id: 'desc' },
});
@@ -520,6 +541,16 @@ export class UsersService {
return parsed;
}
+ /**
+ * 从 actor 上下文读取必填范围字段(医院/科室等)。
+ */
+ private requireActorScopeInt(value: unknown, message: string): number {
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
+ throw new BadRequestException(message);
+ }
+ return value;
+ }
+
/**
* 可空整数标准化。
*/
diff --git a/tyt-admin/src/constants/role-permissions.js b/tyt-admin/src/constants/role-permissions.js
new file mode 100644
index 0000000..d2a6cae
--- /dev/null
+++ b/tyt-admin/src/constants/role-permissions.js
@@ -0,0 +1,37 @@
+/**
+ * 前端页面级权限矩阵:与后端 @Roles 约束保持一致,避免页面可见但接口被 403。
+ */
+const ADMIN_ROLES = Object.freeze(['SYSTEM_ADMIN', 'HOSPITAL_ADMIN']);
+const ORG_MANAGER_ROLES = Object.freeze([
+ 'SYSTEM_ADMIN',
+ 'HOSPITAL_ADMIN',
+ 'DIRECTOR',
+ 'LEADER',
+]);
+const TASK_ROLES = Object.freeze(['DOCTOR', 'DIRECTOR', 'LEADER', 'ENGINEER']);
+const PATIENT_ROLES = Object.freeze([
+ 'SYSTEM_ADMIN',
+ 'HOSPITAL_ADMIN',
+ 'DIRECTOR',
+ 'LEADER',
+]);
+
+export const ROLE_PERMISSIONS = Object.freeze({
+ ORG_TREE: ORG_MANAGER_ROLES,
+ ORG_HOSPITALS: Object.freeze(['SYSTEM_ADMIN']),
+ ORG_DEPARTMENTS: ORG_MANAGER_ROLES,
+ ORG_GROUPS: ORG_MANAGER_ROLES,
+ USERS: ADMIN_ROLES,
+ TASKS: TASK_ROLES,
+ PATIENTS: PATIENT_ROLES,
+});
+
+/**
+ * 判断当前角色是否拥有页面访问权限。
+ */
+export function hasRolePermission(role, allowedRoles) {
+ if (!Array.isArray(allowedRoles) || allowedRoles.length === 0) {
+ return true;
+ }
+ return Boolean(role) && allowedRoles.includes(role);
+}
diff --git a/tyt-admin/src/layouts/AdminLayout.vue b/tyt-admin/src/layouts/AdminLayout.vue
index 20e1539..920748d 100644
--- a/tyt-admin/src/layouts/AdminLayout.vue
+++ b/tyt-admin/src/layouts/AdminLayout.vue
@@ -27,7 +27,7 @@
小组管理
-
+
组织架构图
@@ -42,17 +42,17 @@
-
+
用户管理
-
+
任务管理
-
+
患者管理
@@ -96,6 +96,10 @@
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '../store/user';
+import {
+ ROLE_PERMISSIONS,
+ hasRolePermission,
+} from '../constants/role-permissions';
import { DataLine, OfficeBuilding, User, List, Avatar, ArrowDown, Connection, Share } from '@element-plus/icons-vue';
const route = useRoute();
@@ -106,6 +110,19 @@ const activeMenu = computed(() => {
return route.path;
});
+const canAccessUsers = computed(() =>
+ hasRolePermission(userStore.role, ROLE_PERMISSIONS.USERS),
+);
+const canAccessOrgTree = computed(() =>
+ hasRolePermission(userStore.role, ROLE_PERMISSIONS.ORG_TREE),
+);
+const canAccessTasks = computed(() =>
+ hasRolePermission(userStore.role, ROLE_PERMISSIONS.TASKS),
+);
+const canAccessPatients = computed(() =>
+ hasRolePermission(userStore.role, ROLE_PERMISSIONS.PATIENTS),
+);
+
const handleCommand = (command) => {
if (command === 'logout') {
userStore.logout();
diff --git a/tyt-admin/src/router/index.js b/tyt-admin/src/router/index.js
index 4a9d5dd..e61f38b 100644
--- a/tyt-admin/src/router/index.js
+++ b/tyt-admin/src/router/index.js
@@ -1,7 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
+import { ElMessage } from 'element-plus';
import { useUserStore } from '../store/user';
+import {
+ ROLE_PERMISSIONS,
+ hasRolePermission,
+} from '../constants/role-permissions';
const routes = [
{
@@ -26,43 +31,71 @@ const routes = [
path: 'organization/tree',
name: 'OrgTree',
component: () => import('../views/organization/OrgTree.vue'),
- meta: { title: '组织架构图', requiresAuth: true },
+ meta: {
+ title: '组织架构图',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.ORG_TREE,
+ },
},
{
path: 'organization/hospitals',
name: 'Hospitals',
component: () => import('../views/organization/Hospitals.vue'),
- meta: { title: '医院管理', requiresAuth: true },
+ meta: {
+ title: '医院管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.ORG_HOSPITALS,
+ },
},
{
path: 'organization/departments',
name: 'Departments',
component: () => import('../views/organization/Departments.vue'),
- meta: { title: '科室管理', requiresAuth: true },
+ meta: {
+ title: '科室管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.ORG_DEPARTMENTS,
+ },
},
{
path: 'organization/groups',
name: 'Groups',
component: () => import('../views/organization/Groups.vue'),
- meta: { title: '小组管理', requiresAuth: true },
+ meta: {
+ title: '小组管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.ORG_GROUPS,
+ },
},
{
path: 'users',
name: 'Users',
component: () => import('../views/users/Users.vue'),
- meta: { title: '用户管理', requiresAuth: true },
+ meta: {
+ title: '用户管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.USERS,
+ },
},
{
path: 'tasks',
name: 'Tasks',
component: () => import('../views/tasks/Tasks.vue'),
- meta: { title: '任务管理', requiresAuth: true },
+ meta: {
+ title: '任务管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.TASKS,
+ },
},
{
path: 'patients',
name: 'Patients',
component: () => import('../views/patients/Patients.vue'),
- meta: { title: '患者管理', requiresAuth: true },
+ meta: {
+ title: '患者管理',
+ requiresAuth: true,
+ allowedRoles: ROLE_PERMISSIONS.PATIENTS,
+ },
}
],
},
@@ -87,21 +120,31 @@ router.beforeEach(async (to, from) => {
if (to.meta.requiresAuth && !isLoggedIn) {
return `/login?redirect=${encodeURIComponent(to.fullPath)}`;
- } else if (to.path === '/login' && isLoggedIn) {
+ }
+
+ if (to.path === '/login' && isLoggedIn) {
return '/';
- } else {
- // Attempt to fetch user info if not present but token exists
- if (isLoggedIn && !userStore.userInfo) {
- try {
- await userStore.fetchUserInfo();
- return true;
- } catch (error) {
- return '/login';
- }
- } else {
- return true;
+ }
+
+ // token 存在但内存里无用户信息时,先补拉当前用户上下文。
+ if (isLoggedIn && !userStore.userInfo) {
+ try {
+ await userStore.fetchUserInfo();
+ } catch (error) {
+ return '/login';
}
}
+
+ // 页面级权限校验:无权限时拦截并回到首页,避免进入页面后接口再报 403。
+ if (to.meta.requiresAuth) {
+ const allowedRoles = to.meta.allowedRoles;
+ if (!hasRolePermission(userStore.role, allowedRoles)) {
+ ElMessage.error('当前角色无权限访问该页面');
+ return '/dashboard';
+ }
+ }
+
+ return true;
});
router.afterEach(() => {
diff --git a/tyt-admin/src/views/organization/Departments.vue b/tyt-admin/src/views/organization/Departments.vue
index 011cf8a..31b8d61 100644
--- a/tyt-admin/src/views/organization/Departments.vue
+++ b/tyt-admin/src/views/organization/Departments.vue
@@ -27,7 +27,7 @@
查询
重置
- 新增科室
+ 新增科室
@@ -50,8 +50,8 @@
管理小组
- 编辑
- 删除
+ 编辑
+ 删除
@@ -148,6 +148,15 @@ const rules = computed(() => ({
hospitalId: userStore.role === 'SYSTEM_ADMIN' ? [{ required: true, message: '请选择所属医院', trigger: 'change' }] : [],
name: [{ required: true, message: '请输入科室名称', trigger: 'blur' }]
}));
+const canCreateDepartment = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
+);
+const canEditDepartment = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER'].includes(userStore.role),
+);
+const canDeleteDepartment = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
+);
// --- Methods ---
const fetchHospitals = async () => {
diff --git a/tyt-admin/src/views/organization/Groups.vue b/tyt-admin/src/views/organization/Groups.vue
index 8ab0b99..6378855 100644
--- a/tyt-admin/src/views/organization/Groups.vue
+++ b/tyt-admin/src/views/organization/Groups.vue
@@ -37,7 +37,7 @@
查询
重置
- 新增小组
+ 新增小组
@@ -60,8 +60,8 @@
- 编辑
- 删除
+ 编辑
+ 删除
@@ -178,6 +178,15 @@ const rules = computed(() => ({
departmentId: [{ required: true, message: '请选择所属科室', trigger: 'change' }],
name: [{ required: true, message: '请输入小组名称', trigger: 'blur' }]
}));
+const canCreateGroup = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role),
+);
+const canEditGroup = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER'].includes(userStore.role),
+);
+const canDeleteGroup = computed(() =>
+ ['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role),
+);
// --- Methods ---
const fetchHospitals = async () => {
diff --git a/tyt-admin/src/views/organization/OrgTree.vue b/tyt-admin/src/views/organization/OrgTree.vue
index ef602bb..4c6a5fe 100644
--- a/tyt-admin/src/views/organization/OrgTree.vue
+++ b/tyt-admin/src/views/organization/OrgTree.vue
@@ -62,10 +62,10 @@
>
{{ data.type === 'department' ? '设主任' : '设组长' }}
-
+
编辑
-
+
删除
@@ -84,11 +84,11 @@
@@ -194,7 +194,8 @@ import {
const userStore = useUserStore();
-const canPublishOrCancel = computed(() => userStore.role === 'DOCTOR');
+const PUBLISH_ROLES = ['DOCTOR', 'DIRECTOR', 'LEADER'];
+const canPublishOrCancel = computed(() => PUBLISH_ROLES.includes(userStore.role));
const canAcceptOrComplete = computed(() => userStore.role === 'ENGINEER');
const loading = reactive({