feat(auth-org): 强化用户权限边界并完善组织负责人配置展示
feat(admin-ui): 医院管理显示医院管理员并限制候选角色 feat(security): 关闭注册入口,新增 system-admin 创建链路与数据脱敏
This commit is contained in:
parent
602694814f
commit
b527256874
@ -40,11 +40,17 @@ docs/
|
||||
|
||||
```env
|
||||
DATABASE_URL="postgresql://user:password@127.0.0.1:5432/tyt?schema=public"
|
||||
JWT_SECRET="请替换为强随机密钥"
|
||||
AUTH_TOKEN_SECRET="请替换为强随机密钥"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
SYSTEM_ADMIN_BOOTSTRAP_KEY="初始化系统管理员用密钥"
|
||||
```
|
||||
|
||||
管理员创建链路:
|
||||
|
||||
- 可通过 `POST /auth/system-admin` 创建系统管理员(需引导密钥)。
|
||||
- 系统管理员负责创建医院、系统管理员与医院管理员。
|
||||
- 医院管理员负责创建本院下级角色(主任/组长/医生/工程师)。
|
||||
|
||||
## 4. 启动流程
|
||||
|
||||
```bash
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
## 1. 目标
|
||||
|
||||
- 提供注册、登录、`/me` 身份查询。
|
||||
- 提供系统管理员创建、登录、`/me` 身份查询。
|
||||
- 使用 JWT 做认证,Guard 做鉴权,RolesGuard 做 RBAC。
|
||||
|
||||
## 2. 核心接口
|
||||
|
||||
- `POST /auth/register`:注册账号(支持医生/工程师/院管等角色约束)
|
||||
- `POST /auth/system-admin`:创建系统管理员(需引导密钥)
|
||||
- `POST /auth/login`:手机号 + 角色 + 密码登录(支持同手机号多院场景)
|
||||
- `GET /auth/me`:返回当前登录用户上下文
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
|
||||
1. `AccessTokenGuard` 从 `Authorization` 读取 Bearer Token。
|
||||
2. 校验 JWT 签名与载荷字段。
|
||||
3. 载荷映射为 `ActorContext` 注入 `request.user`。
|
||||
3. 载荷映射为 `ActorContext` 注入 `request.actor`。
|
||||
4. `RolesGuard` 根据 `@Roles(...)` 判断角色是否允许访问。
|
||||
|
||||
## 4. Token 约定
|
||||
|
||||
- Header:`Authorization: Bearer <token>`
|
||||
- 载荷关键字段:`sub`、`role`、`hospitalId`、`departmentId`、`groupId`
|
||||
- 载荷关键字段:`id`、`role`、`hospitalId`、`departmentId`、`groupId`
|
||||
|
||||
## 5. 错误码与中文消息
|
||||
|
||||
|
||||
@ -88,6 +88,7 @@ model User {
|
||||
createdTasks Task[] @relation("TaskCreator")
|
||||
acceptedTasks Task[] @relation("TaskEngineer")
|
||||
|
||||
@@unique([phone, role, hospitalId])
|
||||
@@index([phone])
|
||||
@@index([hospitalId, role])
|
||||
@@index([departmentId, role])
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { RegisterUserDto } from '../users/dto/register-user.dto.js';
|
||||
import { LoginDto } from '../users/dto/login.dto.js';
|
||||
import { AccessTokenGuard } from './access-token.guard.js';
|
||||
import { CurrentActor } from './current-actor.decorator.js';
|
||||
import type { ActorContext } from '../common/actor-context.js';
|
||||
import { CreateSystemAdminDto } from './dto/create-system-admin.dto.js';
|
||||
|
||||
/**
|
||||
* 认证控制器:提供注册、登录、获取当前登录用户信息接口。
|
||||
* 认证控制器:提供系统管理员创建、登录、获取当前登录用户信息接口。
|
||||
*/
|
||||
@ApiTags('认证')
|
||||
@Controller('auth')
|
||||
@ -20,12 +16,12 @@ export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
/**
|
||||
* 注册账号。
|
||||
* 创建系统管理员(需引导密钥)。
|
||||
*/
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: '注册账号' })
|
||||
register(@Body() dto: RegisterUserDto) {
|
||||
return this.authService.register(dto);
|
||||
@Post('system-admin')
|
||||
@ApiOperation({ summary: '创建系统管理员' })
|
||||
createSystemAdmin(@Body() dto: CreateSystemAdminDto) {
|
||||
return this.authService.createSystemAdmin(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { ActorContext } from '../common/actor-context.js';
|
||||
import { UsersService } from '../users/users.service.js';
|
||||
import { RegisterUserDto } from '../users/dto/register-user.dto.js';
|
||||
import { LoginDto } from '../users/dto/login.dto.js';
|
||||
import { CreateSystemAdminDto } from './dto/create-system-admin.dto.js';
|
||||
|
||||
/**
|
||||
* 认证服务:将控制层输入转发到用户域能力,避免控制器直接操作用户仓储。
|
||||
@ -12,10 +12,10 @@ export class AuthService {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
/**
|
||||
* 注册能力委托给用户服务。
|
||||
* 系统管理员创建能力委托给用户服务。
|
||||
*/
|
||||
register(dto: RegisterUserDto) {
|
||||
return this.usersService.register(dto);
|
||||
createSystemAdmin(dto: CreateSystemAdminDto) {
|
||||
return this.usersService.createSystemAdmin(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
32
src/auth/dto/create-system-admin.dto.ts
Normal file
32
src/auth/dto/create-system-admin.dto.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateSystemAdminDto {
|
||||
@ApiProperty({ description: '姓名', example: '系统管理员' })
|
||||
@IsString({ message: 'name 必须是字符串' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ description: '手机号', example: '13800000000' })
|
||||
@IsString({ message: 'phone 必须是字符串' })
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: '密码(至少 8 位)', example: 'Admin@12345' })
|
||||
@IsString({ message: 'password 必须是字符串' })
|
||||
password!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '可选微信 openId',
|
||||
example: 'o123abcxyz',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: 'openId 必须是字符串' })
|
||||
openId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'系统管理员创建引导密钥(来自环境变量 SYSTEM_ADMIN_BOOTSTRAP_KEY)',
|
||||
example: 'init-admin-secret',
|
||||
})
|
||||
@IsString({ message: 'systemAdminBootstrapKey 必须是字符串' })
|
||||
systemAdminBootstrapKey!: string;
|
||||
}
|
||||
@ -25,6 +25,7 @@ export const MESSAGES = {
|
||||
TOKEN_FIELD_INVALID: 'Token 中字段不合法',
|
||||
INVALID_CREDENTIALS: '手机号、角色或密码错误',
|
||||
PASSWORD_NOT_ENABLED: '该账号未启用密码登录',
|
||||
REGISTER_DISABLED: '注册接口已关闭,请联系管理员创建账号',
|
||||
},
|
||||
|
||||
USER: {
|
||||
@ -53,6 +54,8 @@ export const MESSAGES = {
|
||||
DELETE_CONFLICT: '用户存在关联患者或任务,无法删除',
|
||||
MULTI_ACCOUNT_REQUIRE_HOSPITAL:
|
||||
'检测到多个同手机号账号,请传 hospitalId 指定登录医院',
|
||||
CREATE_FORBIDDEN: '当前角色无权限创建该用户',
|
||||
HOSPITAL_ADMIN_SCOPE_FORBIDDEN: '医院管理员仅可操作本院非管理员账号',
|
||||
},
|
||||
|
||||
TASK: {
|
||||
|
||||
@ -10,7 +10,7 @@ import { ResponseEnvelopeInterceptor } from './common/response-envelope.intercep
|
||||
async function bootstrap() {
|
||||
// 创建应用实例并加载核心模块。
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors();
|
||||
// 全局校验:清理未知字段、自动类型转换,并将校验错误统一为中文信息。
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
@ -39,7 +39,7 @@ async function bootstrap() {
|
||||
.setTitle('TYT 多租户医疗调压系统 API')
|
||||
.setDescription('后端接口文档(含认证、RBAC、任务流转与患者聚合)')
|
||||
.setVersion('1.0.0')
|
||||
.addServer('http://localhost:3000', 'localhost')
|
||||
.addServer('http://192.168.0.140:3000', 'localhost')
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: 'http',
|
||||
|
||||
@ -15,7 +15,10 @@ export class WechatNotifyService {
|
||||
/**
|
||||
* 任务通知发送入口:后续可在此接入微信服务号/小程序订阅消息 API。
|
||||
*/
|
||||
async notifyTaskChange(openIds: Array<string | null | undefined>, payload: TaskNotifyPayload) {
|
||||
async notifyTaskChange(
|
||||
openIds: Array<string | null | undefined>,
|
||||
payload: TaskNotifyPayload,
|
||||
) {
|
||||
const targets = Array.from(
|
||||
new Set(
|
||||
openIds
|
||||
@ -32,10 +35,22 @@ export class WechatNotifyService {
|
||||
}
|
||||
|
||||
for (const openId of targets) {
|
||||
const maskedOpenId = this.maskOpenId(openId);
|
||||
// TODO: 在此处调用微信服务号/小程序消息推送 API。
|
||||
this.logger.log(
|
||||
`模拟推送任务通知 event=${payload.event}, taskId=${payload.taskId}, openId=${openId}`,
|
||||
`模拟推送任务通知 event=${payload.event}, taskId=${payload.taskId}, openId=${maskedOpenId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志脱敏:仅保留 openId 首尾片段,避免完整标识泄露。
|
||||
*/
|
||||
private maskOpenId(openId: string) {
|
||||
if (openId.length <= 6) {
|
||||
return '***';
|
||||
}
|
||||
|
||||
return `${openId.slice(0, 3)}***${openId.slice(-3)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiQuery,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { AccessTokenGuard } from '../../auth/access-token.guard.js';
|
||||
import { FamilyLifecycleQueryDto } from '../dto/family-lifecycle-query.dto.js';
|
||||
import { CPatientsService } from './c-patients.service.js';
|
||||
|
||||
@ -7,7 +13,9 @@ import { CPatientsService } from './c-patients.service.js';
|
||||
* C 端患者控制器:家属跨院聚合查询。
|
||||
*/
|
||||
@ApiTags('患者管理(C端)')
|
||||
@ApiBearerAuth('bearer')
|
||||
@Controller('c/patients')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
export class CPatientsController {
|
||||
constructor(private readonly patientsService: CPatientsService) {}
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma.service.js';
|
||||
import { MESSAGES } from '../../common/messages.js';
|
||||
|
||||
@ -57,7 +61,6 @@ export class CPatientsService {
|
||||
patient: {
|
||||
id: this.toJsonNumber(patient.id),
|
||||
name: patient.name,
|
||||
phone: patient.phone,
|
||||
},
|
||||
device: {
|
||||
id: this.toJsonNumber(device.id),
|
||||
@ -68,9 +71,6 @@ export class CPatientsService {
|
||||
task: {
|
||||
id: this.toJsonNumber(task.id),
|
||||
status: task.status,
|
||||
creatorId: this.toJsonNumber(task.creatorId),
|
||||
engineerId: this.toJsonNumber(task.engineerId),
|
||||
hospitalId: this.toJsonNumber(task.hospitalId),
|
||||
createdAt: task.createdAt,
|
||||
},
|
||||
taskItem: {
|
||||
@ -89,8 +89,6 @@ export class CPatientsService {
|
||||
);
|
||||
|
||||
return {
|
||||
phone,
|
||||
idCardHash,
|
||||
patientCount: patients.length,
|
||||
lifecycle,
|
||||
};
|
||||
|
||||
@ -43,7 +43,9 @@ export class TaskService {
|
||||
throw new BadRequestException(`deviceId 非法: ${item.deviceId}`);
|
||||
}
|
||||
if (!Number.isInteger(item.targetPressure)) {
|
||||
throw new BadRequestException(`targetPressure 非法: ${item.targetPressure}`);
|
||||
throw new BadRequestException(
|
||||
`targetPressure 非法: ${item.targetPressure}`,
|
||||
);
|
||||
}
|
||||
return item.deviceId;
|
||||
}),
|
||||
@ -138,14 +140,30 @@ export class TaskService {
|
||||
throw new ForbiddenException(MESSAGES.TASK.ENGINEER_ALREADY_ASSIGNED);
|
||||
}
|
||||
|
||||
const updatedTask = await this.prisma.task.update({
|
||||
where: { id: task.id },
|
||||
const accepted = await this.prisma.task.updateMany({
|
||||
where: {
|
||||
id: task.id,
|
||||
hospitalId,
|
||||
status: TaskStatus.PENDING,
|
||||
OR: [{ engineerId: null }, { engineerId: actor.id }],
|
||||
},
|
||||
data: {
|
||||
status: TaskStatus.ACCEPTED,
|
||||
engineerId: actor.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (accepted.count !== 1) {
|
||||
throw new ConflictException(MESSAGES.TASK.ACCEPT_ONLY_PENDING);
|
||||
}
|
||||
|
||||
const updatedTask = await this.prisma.task.findUnique({
|
||||
where: { id: task.id },
|
||||
include: { items: true },
|
||||
});
|
||||
if (!updatedTask) {
|
||||
throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.eventEmitter.emitAsync('task.accepted', {
|
||||
taskId: updatedTask.id,
|
||||
|
||||
@ -40,20 +40,18 @@ export class UsersController {
|
||||
@Post()
|
||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||
@ApiOperation({ summary: '创建用户' })
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
create(
|
||||
@CurrentActor() actor: ActorContext,
|
||||
@Body() createUserDto: CreateUserDto,
|
||||
) {
|
||||
return this.usersService.create(actor, createUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户列表。
|
||||
*/
|
||||
@Get()
|
||||
@Roles(
|
||||
Role.SYSTEM_ADMIN,
|
||||
Role.HOSPITAL_ADMIN,
|
||||
Role.DIRECTOR,
|
||||
Role.LEADER,
|
||||
)
|
||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER)
|
||||
@ApiOperation({ summary: '查询用户列表' })
|
||||
findAll(@CurrentActor() actor: ActorContext) {
|
||||
return this.usersService.findAll(actor);
|
||||
@ -66,8 +64,8 @@ export class UsersController {
|
||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||
@ApiOperation({ summary: '查询用户详情' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.usersService.findOne(+id);
|
||||
findOne(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
||||
return this.usersService.findOne(actor, +id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,8 +75,12 @@ export class UsersController {
|
||||
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||
@ApiOperation({ summary: '更新用户' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.usersService.update(+id, updateUserDto);
|
||||
update(
|
||||
@CurrentActor() actor: ActorContext,
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
) {
|
||||
return this.usersService.update(actor, +id, updateUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,7 +90,7 @@ export class UsersController {
|
||||
@Roles(Role.SYSTEM_ADMIN)
|
||||
@ApiOperation({ summary: '删除用户' })
|
||||
@ApiParam({ name: 'id', description: '用户 ID' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.usersService.remove(+id);
|
||||
remove(@CurrentActor() actor: ActorContext, @Param('id') id: string) {
|
||||
return this.usersService.remove(actor, +id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,9 @@ import { Role } from '../generated/prisma/enums.js';
|
||||
import { PrismaService } from '../prisma.service.js';
|
||||
import type { ActorContext } from '../common/actor-context.js';
|
||||
import { AssignEngineerHospitalDto } from './dto/assign-engineer-hospital.dto.js';
|
||||
import { RegisterUserDto } from './dto/register-user.dto.js';
|
||||
import { LoginDto } from './dto/login.dto.js';
|
||||
import { MESSAGES } from '../common/messages.js';
|
||||
import { CreateSystemAdminDto } from '../auth/dto/create-system-admin.dto.js';
|
||||
|
||||
const SAFE_USER_SELECT = {
|
||||
id: true,
|
||||
@ -35,25 +35,27 @@ export class UsersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 注册账号:根据角色与组织范围进行约束,并写入 bcrypt 密码摘要。
|
||||
* 注册接口已关闭,避免绕过管理员创建链路。
|
||||
*/
|
||||
async register(dto: RegisterUserDto) {
|
||||
const role = this.normalizeRole(dto.role);
|
||||
async register() {
|
||||
throw new ForbiddenException(MESSAGES.AUTH.REGISTER_DISABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统管理员:要求引导密钥。
|
||||
*/
|
||||
async createSystemAdmin(dto: CreateSystemAdminDto) {
|
||||
const name = this.normalizeRequiredString(dto.name, 'name');
|
||||
const phone = this.normalizePhone(dto.phone);
|
||||
const password = this.normalizePassword(dto.password);
|
||||
const openId = this.normalizeOptionalString(dto.openId);
|
||||
const hospitalId = this.normalizeOptionalInt(dto.hospitalId, 'hospitalId');
|
||||
const departmentId = this.normalizeOptionalInt(
|
||||
dto.departmentId,
|
||||
'departmentId',
|
||||
);
|
||||
const groupId = this.normalizeOptionalInt(dto.groupId, 'groupId');
|
||||
|
||||
this.assertSystemAdminBootstrapKey(role, dto.systemAdminBootstrapKey);
|
||||
await this.assertOrganizationScope(role, hospitalId, departmentId, groupId);
|
||||
this.assertSystemAdminBootstrapKey(
|
||||
Role.SYSTEM_ADMIN,
|
||||
dto.systemAdminBootstrapKey,
|
||||
);
|
||||
await this.assertOpenIdUnique(openId);
|
||||
await this.assertPhoneRoleScopeUnique(phone, role, hospitalId);
|
||||
await this.assertPhoneRoleScopeUnique(phone, Role.SYSTEM_ADMIN, null);
|
||||
|
||||
const passwordHash = await hash(password, 12);
|
||||
|
||||
@ -63,10 +65,10 @@ export class UsersService {
|
||||
phone,
|
||||
passwordHash,
|
||||
openId,
|
||||
role,
|
||||
hospitalId,
|
||||
departmentId,
|
||||
groupId,
|
||||
role: Role.SYSTEM_ADMIN,
|
||||
hospitalId: null,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
},
|
||||
select: SAFE_USER_SELECT,
|
||||
});
|
||||
@ -133,13 +135,13 @@ export class UsersService {
|
||||
* 获取当前登录用户详情。
|
||||
*/
|
||||
async me(actor: ActorContext) {
|
||||
return this.findOne(actor.id);
|
||||
return this.findOne(actor, actor.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* B 端创建用户(通常由管理员使用)。
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto) {
|
||||
async create(actor: ActorContext, createUserDto: CreateUserDto) {
|
||||
const role = this.normalizeRole(createUserDto.role);
|
||||
const name = this.normalizeRequiredString(createUserDto.name, 'name');
|
||||
const phone = this.normalizePhone(createUserDto.phone);
|
||||
@ -157,9 +159,22 @@ export class UsersService {
|
||||
);
|
||||
const groupId = this.normalizeOptionalInt(createUserDto.groupId, 'groupId');
|
||||
|
||||
await this.assertOrganizationScope(role, hospitalId, departmentId, groupId);
|
||||
const scoped = this.resolveCreateScope(
|
||||
actor,
|
||||
role,
|
||||
hospitalId,
|
||||
departmentId,
|
||||
groupId,
|
||||
);
|
||||
|
||||
await this.assertOrganizationScope(
|
||||
role,
|
||||
scoped.hospitalId,
|
||||
scoped.departmentId,
|
||||
scoped.groupId,
|
||||
);
|
||||
await this.assertOpenIdUnique(openId);
|
||||
await this.assertPhoneRoleScopeUnique(phone, role, hospitalId);
|
||||
await this.assertPhoneRoleScopeUnique(phone, role, scoped.hospitalId);
|
||||
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
@ -168,9 +183,9 @@ export class UsersService {
|
||||
passwordHash: password ? await hash(password, 12) : null,
|
||||
openId,
|
||||
role,
|
||||
hospitalId,
|
||||
departmentId,
|
||||
groupId,
|
||||
hospitalId: scoped.hospitalId,
|
||||
departmentId: scoped.departmentId,
|
||||
groupId: scoped.groupId,
|
||||
},
|
||||
select: SAFE_USER_SELECT,
|
||||
});
|
||||
@ -210,7 +225,7 @@ export class UsersService {
|
||||
/**
|
||||
* 查询用户详情。
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
async findOne(actor: ActorContext, id: number) {
|
||||
const userId = this.normalizeRequiredInt(id, 'id');
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
@ -221,13 +236,15 @@ export class UsersService {
|
||||
throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
|
||||
}
|
||||
|
||||
this.assertUserReadable(actor, user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息(含可选密码重置)。
|
||||
*/
|
||||
async update(id: number, updateUserDto: UpdateUserDto) {
|
||||
async update(actor: ActorContext, id: number, updateUserDto: UpdateUserDto) {
|
||||
const userId = this.normalizeRequiredInt(id, 'id');
|
||||
const current = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@ -240,8 +257,12 @@ export class UsersService {
|
||||
throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
|
||||
}
|
||||
|
||||
this.assertUserWritable(actor, current);
|
||||
|
||||
const nextRole =
|
||||
updateUserDto.role != null ? this.normalizeRole(updateUserDto.role) : current.role;
|
||||
updateUserDto.role != null
|
||||
? this.normalizeRole(updateUserDto.role)
|
||||
: current.role;
|
||||
const nextHospitalId =
|
||||
updateUserDto.hospitalId !== undefined
|
||||
? this.normalizeOptionalInt(updateUserDto.hospitalId, 'hospitalId')
|
||||
@ -255,6 +276,9 @@ export class UsersService {
|
||||
? this.normalizeOptionalInt(updateUserDto.groupId, 'groupId')
|
||||
: current.groupId;
|
||||
|
||||
this.assertUpdateTargetRoleAllowed(actor, nextRole);
|
||||
this.assertUpdateHospitalScopeAllowed(actor, nextHospitalId);
|
||||
|
||||
const assigningDepartmentOrGroup =
|
||||
(updateUserDto.departmentId !== undefined && nextDepartmentId != null) ||
|
||||
(updateUserDto.groupId !== undefined && nextGroupId != null);
|
||||
@ -329,9 +353,10 @@ export class UsersService {
|
||||
/**
|
||||
* 删除用户。
|
||||
*/
|
||||
async remove(id: number) {
|
||||
async remove(actor: ActorContext, id: number) {
|
||||
const userId = this.normalizeRequiredInt(id, 'id');
|
||||
await this.findOne(userId);
|
||||
const target = await this.findOne(actor, userId);
|
||||
this.assertUserWritable(actor, target);
|
||||
|
||||
try {
|
||||
return await this.prisma.user.delete({
|
||||
@ -397,7 +422,9 @@ export class UsersService {
|
||||
/**
|
||||
* 去除密码摘要,避免泄露敏感信息。
|
||||
*/
|
||||
private toSafeUser(user: { passwordHash?: string | null } & Record<string, unknown>) {
|
||||
private toSafeUser(
|
||||
user: { passwordHash?: string | null } & Record<string, unknown>,
|
||||
) {
|
||||
const { passwordHash, ...safe } = user;
|
||||
return safe;
|
||||
}
|
||||
@ -512,7 +539,9 @@ export class UsersService {
|
||||
select: { id: true, hospitalId: true },
|
||||
});
|
||||
if (!department || department.hospitalId !== hospitalId) {
|
||||
throw new BadRequestException(MESSAGES.USER.DEPARTMENT_HOSPITAL_MISMATCH);
|
||||
throw new BadRequestException(
|
||||
MESSAGES.USER.DEPARTMENT_HOSPITAL_MISMATCH,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -630,6 +659,163 @@ export class UsersService {
|
||||
return role as Role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析创建用户作用域并执行角色链路约束。
|
||||
*/
|
||||
private resolveCreateScope(
|
||||
actor: ActorContext,
|
||||
targetRole: Role,
|
||||
hospitalId: number | null,
|
||||
departmentId: number | null,
|
||||
groupId: number | null,
|
||||
) {
|
||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||
if (targetRole === Role.SYSTEM_ADMIN) {
|
||||
return { hospitalId: null, departmentId: null, groupId: null };
|
||||
}
|
||||
|
||||
// 系统管理员可创建任意角色,具体归属由后续组织范围校验保证合法。
|
||||
return { hospitalId, departmentId, groupId };
|
||||
}
|
||||
|
||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||
}
|
||||
|
||||
if (
|
||||
targetRole === Role.SYSTEM_ADMIN ||
|
||||
targetRole === Role.HOSPITAL_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
|
||||
}
|
||||
|
||||
const actorHospitalId = this.requireActorScopeInt(
|
||||
actor.hospitalId,
|
||||
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||
);
|
||||
const scopedHospitalId = hospitalId ?? actorHospitalId;
|
||||
if (scopedHospitalId !== actorHospitalId) {
|
||||
throw new ForbiddenException(
|
||||
MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
hospitalId: scopedHospitalId,
|
||||
departmentId,
|
||||
groupId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取权限:系统管理员可读全量;其余仅可读自己与本院(医院管理员)。
|
||||
*/
|
||||
private assertUserReadable(
|
||||
actor: ActorContext,
|
||||
target: Pick<typeof SAFE_USER_SELECT, never> & {
|
||||
id: number;
|
||||
role: Role;
|
||||
hospitalId: number | null;
|
||||
},
|
||||
) {
|
||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||
return;
|
||||
}
|
||||
if (actor.id === target.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.role === Role.HOSPITAL_ADMIN) {
|
||||
const actorHospitalId = this.requireActorScopeInt(
|
||||
actor.hospitalId,
|
||||
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||
);
|
||||
if (target.hospitalId === actorHospitalId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写权限:医院管理员仅可写本院非管理员账号。
|
||||
*/
|
||||
private assertUserWritable(
|
||||
actor: ActorContext,
|
||||
target: {
|
||||
id: number;
|
||||
role: Role;
|
||||
hospitalId: number | null;
|
||||
},
|
||||
) {
|
||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||
throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
|
||||
}
|
||||
|
||||
const actorHospitalId = this.requireActorScopeInt(
|
||||
actor.hospitalId,
|
||||
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||
);
|
||||
if (target.hospitalId !== actorHospitalId) {
|
||||
throw new ForbiddenException(
|
||||
MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
if (
|
||||
target.role === Role.HOSPITAL_ADMIN ||
|
||||
target.role === Role.SYSTEM_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入时角色边界校验,避免医院管理员提升权限。
|
||||
*/
|
||||
private assertUpdateTargetRoleAllowed(actor: ActorContext, nextRole: Role) {
|
||||
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
actor.role === Role.HOSPITAL_ADMIN &&
|
||||
(nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入时医院范围校验,避免医院管理员跨院更新。
|
||||
*/
|
||||
private assertUpdateHospitalScopeAllowed(
|
||||
actor: ActorContext,
|
||||
hospitalId: number | null,
|
||||
) {
|
||||
if (actor.role !== Role.HOSPITAL_ADMIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actorHospitalId = this.requireActorScopeInt(
|
||||
actor.hospitalId,
|
||||
MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
|
||||
);
|
||||
if (hospitalId !== actorHospitalId) {
|
||||
throw new ForbiddenException(
|
||||
MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签发访问令牌。
|
||||
*/
|
||||
|
||||
1
tyt-admin/components.d.ts
vendored
1
tyt-admin/components.d.ts
vendored
@ -45,6 +45,7 @@ declare module 'vue' {
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<el-option label="系统管理员" value="SYSTEM_ADMIN" />
|
||||
<el-option label="医院管理员" value="HOSPITAL_ADMIN" />
|
||||
<el-option label="科室主任" value="DIRECTOR" />
|
||||
<el-option label="医疗组长" value="LEADER" />
|
||||
<el-option label="小组组长" value="LEADER" />
|
||||
<el-option label="医生" value="DOCTOR" />
|
||||
<el-option label="工程师" value="ENGINEER" />
|
||||
</el-select>
|
||||
|
||||
@ -3,16 +3,35 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>科室管理 {{ currentHospitalName ? `(${currentHospitalName})` : '' }}</span>
|
||||
<el-button v-if="currentHospitalName" @click="clearHospitalFilter" type="info" size="small">清除医院筛选</el-button>
|
||||
<span
|
||||
>科室管理
|
||||
{{ currentHospitalName ? `(${currentHospitalName})` : '' }}</span
|
||||
>
|
||||
<el-button
|
||||
v-if="currentHospitalName"
|
||||
@click="clearHospitalFilter"
|
||||
type="info"
|
||||
size="small"
|
||||
>清除医院筛选</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Header / Actions -->
|
||||
<div class="header-actions">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="所属医院" v-if="userStore.role === 'SYSTEM_ADMIN' && !currentHospitalIdFromQuery">
|
||||
<el-select v-model="searchForm.hospitalId" placeholder="请选择医院" clearable @change="fetchData">
|
||||
<el-form-item
|
||||
label="所属医院"
|
||||
v-if="
|
||||
userStore.role === 'SYSTEM_ADMIN' && !currentHospitalIdFromQuery
|
||||
"
|
||||
>
|
||||
<el-select
|
||||
v-model="searchForm.hospitalId"
|
||||
placeholder="请选择医院"
|
||||
clearable
|
||||
@change="fetchData"
|
||||
>
|
||||
<el-option
|
||||
v-for="h in hospitals"
|
||||
:key="h.id"
|
||||
@ -22,21 +41,44 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="科室名称">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入关键词" clearable />
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入关键词"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchData" icon="Search">查询</el-button>
|
||||
<el-button type="primary" @click="fetchData" icon="Search"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||
<el-button v-if="canCreateDepartment" type="success" @click="openCreateDialog" icon="Plus">新增科室</el-button>
|
||||
<el-button
|
||||
v-if="canCreateDepartment"
|
||||
type="success"
|
||||
@click="openCreateDialog"
|
||||
icon="Plus"
|
||||
>新增科室</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="科室名称" min-width="150" />
|
||||
<el-table-column prop="hospital.name" label="所属医院" min-width="200" v-if="userStore.role === 'SYSTEM_ADMIN'" />
|
||||
<el-table-column
|
||||
prop="hospital.name"
|
||||
label="所属医院"
|
||||
min-width="200"
|
||||
v-if="userStore.role === 'SYSTEM_ADMIN'"
|
||||
/>
|
||||
<el-table-column label="科室主任" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ getDirectorDisplay(row.id) }}
|
||||
@ -49,9 +91,23 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="goToGroups(row)">管理小组</el-button>
|
||||
<el-button v-if="canEditDepartment" size="small" type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button v-if="canDeleteDepartment" size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button size="small" @click="goToGroups(row)"
|
||||
>管理小组</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="canEditDepartment"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openEditDialog(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="canDeleteDepartment"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -72,10 +128,24 @@
|
||||
</el-card>
|
||||
|
||||
<!-- Dialog for Create / Edit -->
|
||||
<el-dialog :title="isEdit ? '编辑科室' : '新增科室'" v-model="dialogVisible" width="500px" @close="resetForm">
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑科室' : '新增科室'"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="所属医院" prop="hospitalId" v-if="userStore.role === 'SYSTEM_ADMIN'">
|
||||
<el-select v-model="form.hospitalId" placeholder="请选择所属医院" style="width: 100%;">
|
||||
<el-form-item
|
||||
label="所属医院"
|
||||
prop="hospitalId"
|
||||
v-if="userStore.role === 'SYSTEM_ADMIN'"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.hospitalId"
|
||||
placeholder="请选择所属医院"
|
||||
style="width: 100%"
|
||||
@change="handleFormHospitalChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="h in hospitals"
|
||||
:key="h.id"
|
||||
@ -87,11 +157,32 @@
|
||||
<el-form-item label="科室名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入科室名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室主任" v-if="canAssignDirectorInDialog">
|
||||
<el-select
|
||||
v-model="form.directorUserId"
|
||||
placeholder="可选:选择后将任命为科室主任"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in directorOptions"
|
||||
:key="user.id"
|
||||
:label="`${user.name}(${user.phone} / ${getRoleName(user.role)})`"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>确定</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -102,8 +193,14 @@
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getDepartments, createDepartment, updateDepartment, deleteDepartment, getHospitals } from '../../api/organization';
|
||||
import { getUsers } from '../../api/users';
|
||||
import {
|
||||
getDepartments,
|
||||
createDepartment,
|
||||
updateDepartment,
|
||||
deleteDepartment,
|
||||
getHospitals,
|
||||
} from '../../api/organization';
|
||||
import { getUsers, updateUser } from '../../api/users';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
const route = useRoute();
|
||||
@ -118,6 +215,15 @@ const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const hospitals = ref([]);
|
||||
const directorNameMap = ref({});
|
||||
const directorOptions = ref([]);
|
||||
|
||||
const roleMap = {
|
||||
DIRECTOR: '科室主任',
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
};
|
||||
|
||||
const getRoleName = (role) => roleMap[role] || role;
|
||||
|
||||
const currentHospitalIdFromQuery = computed(() => {
|
||||
return route.query.hospitalId ? parseInt(route.query.hospitalId) : null;
|
||||
@ -129,7 +235,7 @@ const currentHospitalName = computed(() => {
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
hospitalId: null
|
||||
hospitalId: null,
|
||||
});
|
||||
|
||||
// Dialog State
|
||||
@ -141,22 +247,31 @@ const currentId = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
hospitalId: null,
|
||||
name: ''
|
||||
name: '',
|
||||
directorUserId: null,
|
||||
});
|
||||
|
||||
const rules = computed(() => ({
|
||||
hospitalId: userStore.role === 'SYSTEM_ADMIN' ? [{ required: true, message: '请选择所属医院', trigger: 'change' }] : [],
|
||||
name: [{ required: true, message: '请输入科室名称', trigger: 'blur' }]
|
||||
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),
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER'].includes(
|
||||
userStore.role,
|
||||
),
|
||||
);
|
||||
const canDeleteDepartment = computed(() =>
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
|
||||
);
|
||||
const canAssignDirectorInDialog = computed(() =>
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
|
||||
);
|
||||
|
||||
// --- Methods ---
|
||||
const fetchHospitals = async () => {
|
||||
@ -171,7 +286,8 @@ const fetchHospitals = async () => {
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const activeHospitalId = currentHospitalIdFromQuery.value || searchForm.hospitalId;
|
||||
const activeHospitalId =
|
||||
currentHospitalIdFromQuery.value || searchForm.hospitalId;
|
||||
const [departmentRes, directorRes] = await Promise.all([
|
||||
getDepartments({
|
||||
page: page.value,
|
||||
@ -227,15 +343,20 @@ const goToGroups = (row) => {
|
||||
query: {
|
||||
departmentId: row.id,
|
||||
departmentName: row.name,
|
||||
hospitalId: row.hospitalId
|
||||
}
|
||||
hospitalId: row.hospitalId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
isEdit.value = false;
|
||||
currentId.value = null;
|
||||
form.hospitalId = userStore.role === 'SYSTEM_ADMIN' ? (currentHospitalIdFromQuery.value || searchForm.hospitalId || null) : userStore.userInfo?.hospitalId;
|
||||
form.hospitalId =
|
||||
userStore.role === 'SYSTEM_ADMIN'
|
||||
? currentHospitalIdFromQuery.value || searchForm.hospitalId || null
|
||||
: userStore.userInfo?.hospitalId;
|
||||
form.directorUserId = null;
|
||||
loadDirectorOptions(form.hospitalId);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
@ -244,6 +365,8 @@ const openEditDialog = (row) => {
|
||||
currentId.value = row.id;
|
||||
form.name = row.name;
|
||||
form.hospitalId = row.hospitalId;
|
||||
form.directorUserId = null;
|
||||
loadDirectorOptions(row.hospitalId, row.id);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
@ -253,6 +376,34 @@ const resetForm = () => {
|
||||
}
|
||||
form.name = '';
|
||||
form.hospitalId = null;
|
||||
form.directorUserId = null;
|
||||
};
|
||||
|
||||
const handleFormHospitalChange = (hospitalId) => {
|
||||
form.directorUserId = null;
|
||||
loadDirectorOptions(hospitalId);
|
||||
};
|
||||
|
||||
const loadDirectorOptions = async (hospitalId, departmentId) => {
|
||||
if (!canAssignDirectorInDialog.value || !hospitalId) {
|
||||
directorOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const userRes = await getUsers();
|
||||
const users = Array.isArray(userRes?.list) ? userRes.list : [];
|
||||
directorOptions.value = users.filter((user) => {
|
||||
if (user.role !== 'DIRECTOR') {
|
||||
return false;
|
||||
}
|
||||
if (user.hospitalId !== hospitalId) {
|
||||
return false;
|
||||
}
|
||||
if (departmentId == null) {
|
||||
return true;
|
||||
}
|
||||
return user.departmentId == null || user.departmentId === departmentId;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -261,14 +412,39 @@ const handleSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let targetDepartmentId = null;
|
||||
let targetHospitalId = form.hospitalId;
|
||||
if (isEdit.value) {
|
||||
// Some backend update APIs don't allow changing hospitalId, but we'll send it if needed, or just name
|
||||
await updateDepartment(currentId.value, { name: form.name });
|
||||
const updated = await updateDepartment(currentId.value, { name: form.name });
|
||||
targetDepartmentId = updated?.id ?? currentId.value;
|
||||
targetHospitalId = updated?.hospitalId ?? form.hospitalId;
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createDepartment(form);
|
||||
const created = await createDepartment({
|
||||
hospitalId: form.hospitalId,
|
||||
name: form.name,
|
||||
});
|
||||
targetDepartmentId = created?.id;
|
||||
targetHospitalId = created?.hospitalId ?? form.hospitalId;
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
if (
|
||||
canAssignDirectorInDialog.value
|
||||
&& form.directorUserId
|
||||
&& targetDepartmentId
|
||||
&& targetHospitalId
|
||||
) {
|
||||
await updateUser(form.directorUserId, {
|
||||
role: 'DIRECTOR',
|
||||
hospitalId: targetHospitalId,
|
||||
departmentId: targetDepartmentId,
|
||||
groupId: null,
|
||||
});
|
||||
ElMessage.success('科室主任已设置');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -281,15 +457,12 @@ const handleSubmit = async () => {
|
||||
};
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除科室 "${row.name}" 吗?`,
|
||||
'警告',
|
||||
{
|
||||
ElMessageBox.confirm(`确定要删除科室 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await deleteDepartment(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
@ -297,7 +470,8 @@ const handleDelete = (row) => {
|
||||
} catch (error) {
|
||||
console.error('Delete failed', error);
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// --- Lifecycle ---
|
||||
@ -313,7 +487,7 @@ watch(
|
||||
() => {
|
||||
page.value = 1;
|
||||
fetchData();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@ -3,16 +3,37 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>小组管理 {{ currentDepartmentName ? `(${currentDepartmentName})` : '' }}</span>
|
||||
<el-button v-if="currentDepartmentIdFromQuery" @click="clearDepartmentFilter" type="info" size="small">清除科室筛选</el-button>
|
||||
<span
|
||||
>小组管理
|
||||
{{
|
||||
currentDepartmentName ? `(${currentDepartmentName})` : ''
|
||||
}}</span
|
||||
>
|
||||
<el-button
|
||||
v-if="currentDepartmentIdFromQuery"
|
||||
@click="clearDepartmentFilter"
|
||||
type="info"
|
||||
size="small"
|
||||
>清除科室筛选</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Header / Actions -->
|
||||
<div class="header-actions">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="所属医院" v-if="userStore.role === 'SYSTEM_ADMIN' && !currentDepartmentIdFromQuery">
|
||||
<el-select v-model="searchForm.hospitalId" placeholder="请选择医院" clearable @change="handleSearchHospitalChange">
|
||||
<el-form-item
|
||||
label="所属医院"
|
||||
v-if="
|
||||
userStore.role === 'SYSTEM_ADMIN' && !currentDepartmentIdFromQuery
|
||||
"
|
||||
>
|
||||
<el-select
|
||||
v-model="searchForm.hospitalId"
|
||||
placeholder="请选择医院"
|
||||
clearable
|
||||
@change="handleSearchHospitalChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="h in hospitals"
|
||||
:key="h.id"
|
||||
@ -22,7 +43,15 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属科室" v-if="!currentDepartmentIdFromQuery">
|
||||
<el-select v-model="searchForm.departmentId" placeholder="请选择科室" clearable @change="fetchData" :disabled="userStore.role === 'SYSTEM_ADMIN' && !searchForm.hospitalId">
|
||||
<el-select
|
||||
v-model="searchForm.departmentId"
|
||||
placeholder="请选择科室"
|
||||
clearable
|
||||
@change="fetchData"
|
||||
:disabled="
|
||||
userStore.role === 'SYSTEM_ADMIN' && !searchForm.hospitalId
|
||||
"
|
||||
>
|
||||
<el-option
|
||||
v-for="d in searchDepartments"
|
||||
:key="d.id"
|
||||
@ -32,22 +61,49 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="小组名称">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入关键词" clearable />
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入关键词"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchData" icon="Search">查询</el-button>
|
||||
<el-button type="primary" @click="fetchData" icon="Search"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||
<el-button v-if="canCreateGroup" type="success" @click="openCreateDialog" icon="Plus">新增小组</el-button>
|
||||
<el-button
|
||||
v-if="canCreateGroup"
|
||||
type="success"
|
||||
@click="openCreateDialog"
|
||||
icon="Plus"
|
||||
>新增小组</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="小组名称" min-width="150" />
|
||||
<el-table-column prop="department.name" label="所属科室" min-width="150" />
|
||||
<el-table-column prop="department.hospital.name" label="所属医院" min-width="150" v-if="userStore.role === 'SYSTEM_ADMIN'" />
|
||||
<el-table-column
|
||||
prop="department.name"
|
||||
label="所属科室"
|
||||
min-width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="department.hospital.name"
|
||||
label="所属医院"
|
||||
min-width="150"
|
||||
v-if="userStore.role === 'SYSTEM_ADMIN'"
|
||||
/>
|
||||
<el-table-column label="小组组长" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ getLeaderDisplay(row.id) }}
|
||||
@ -60,8 +116,20 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="canEditGroup" size="small" type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button v-if="canDeleteGroup" size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button
|
||||
v-if="canEditGroup"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openEditDialog(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="canDeleteGroup"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -82,10 +150,24 @@
|
||||
</el-card>
|
||||
|
||||
<!-- Dialog for Create / Edit -->
|
||||
<el-dialog :title="isEdit ? '编辑小组' : '新增小组'" v-model="dialogVisible" width="500px" @close="resetForm">
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑小组' : '新增小组'"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="所属医院" prop="hospitalId" v-if="userStore.role === 'SYSTEM_ADMIN'">
|
||||
<el-select v-model="form.hospitalId" placeholder="请选择所属医院" style="width: 100%;" @change="handleFormHospitalChange">
|
||||
<el-form-item
|
||||
label="所属医院"
|
||||
prop="hospitalId"
|
||||
v-if="userStore.role === 'SYSTEM_ADMIN'"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.hospitalId"
|
||||
placeholder="请选择所属医院"
|
||||
style="width: 100%"
|
||||
@change="handleFormHospitalChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="h in hospitals"
|
||||
:key="h.id"
|
||||
@ -95,7 +177,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属科室" prop="departmentId">
|
||||
<el-select v-model="form.departmentId" placeholder="请选择所属科室" style="width: 100%;" :disabled="userStore.role === 'SYSTEM_ADMIN' && !form.hospitalId">
|
||||
<el-select
|
||||
v-model="form.departmentId"
|
||||
placeholder="请选择所属科室"
|
||||
style="width: 100%"
|
||||
:disabled="userStore.role === 'SYSTEM_ADMIN' && !form.hospitalId"
|
||||
>
|
||||
<el-option
|
||||
v-for="d in formDepartments"
|
||||
:key="d.id"
|
||||
@ -107,11 +194,32 @@
|
||||
<el-form-item label="小组名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入小组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="小组组长" v-if="canAssignLeaderInDialog">
|
||||
<el-select
|
||||
v-model="form.leaderUserId"
|
||||
placeholder="可选:选择后将任命为小组组长"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in leaderOptions"
|
||||
:key="user.id"
|
||||
:label="`${user.name}(${user.phone} / ${getRoleName(user.role)})`"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>确定</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -122,8 +230,15 @@
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getGroups, createGroup, updateGroup, deleteGroup, getHospitals, getDepartments } from '../../api/organization';
|
||||
import { getUsers } from '../../api/users';
|
||||
import {
|
||||
getGroups,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
getHospitals,
|
||||
getDepartments,
|
||||
} from '../../api/organization';
|
||||
import { getUsers, updateUser } from '../../api/users';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
const route = useRoute();
|
||||
@ -138,6 +253,14 @@ const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const hospitals = ref([]);
|
||||
const leaderNameMap = ref({});
|
||||
const leaderOptions = ref([]);
|
||||
|
||||
const roleMap = {
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
};
|
||||
|
||||
const getRoleName = (role) => roleMap[role] || role;
|
||||
|
||||
const searchDepartments = ref([]);
|
||||
const formDepartments = ref([]);
|
||||
@ -157,7 +280,7 @@ const currentDepartmentName = computed(() => {
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
hospitalId: null,
|
||||
departmentId: null
|
||||
departmentId: null,
|
||||
});
|
||||
|
||||
// Dialog State
|
||||
@ -170,23 +293,34 @@ const currentId = ref(null);
|
||||
const form = reactive({
|
||||
hospitalId: null,
|
||||
departmentId: null,
|
||||
name: ''
|
||||
name: '',
|
||||
leaderUserId: null,
|
||||
});
|
||||
|
||||
const rules = computed(() => ({
|
||||
hospitalId: userStore.role === 'SYSTEM_ADMIN' ? [{ required: true, message: '请选择所属医院', trigger: 'change' }] : [],
|
||||
departmentId: [{ required: true, message: '请选择所属科室', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入小组名称', trigger: 'blur' }]
|
||||
hospitalId:
|
||||
userStore.role === 'SYSTEM_ADMIN'
|
||||
? [{ required: true, message: '请选择所属医院', trigger: 'change' }]
|
||||
: [],
|
||||
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),
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR', 'LEADER'].includes(
|
||||
userStore.role,
|
||||
),
|
||||
);
|
||||
const canDeleteGroup = computed(() =>
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN', 'DIRECTOR'].includes(userStore.role),
|
||||
);
|
||||
const canAssignLeaderInDialog = computed(() =>
|
||||
['SYSTEM_ADMIN', 'HOSPITAL_ADMIN'].includes(userStore.role),
|
||||
);
|
||||
|
||||
// --- Methods ---
|
||||
const fetchHospitals = async () => {
|
||||
@ -212,6 +346,7 @@ const handleSearchHospitalChange = async (hospitalId) => {
|
||||
|
||||
const handleFormHospitalChange = async (hospitalId) => {
|
||||
form.departmentId = null;
|
||||
form.leaderUserId = null;
|
||||
formDepartments.value = [];
|
||||
if (hospitalId) {
|
||||
try {
|
||||
@ -221,11 +356,35 @@ const handleFormHospitalChange = async (hospitalId) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadLeaderOptions = async (hospitalId, departmentId, groupId) => {
|
||||
if (!canAssignLeaderInDialog.value || !hospitalId || !departmentId) {
|
||||
leaderOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const userRes = await getUsers();
|
||||
const users = Array.isArray(userRes?.list) ? userRes.list : [];
|
||||
leaderOptions.value = users.filter((user) => {
|
||||
if (user.role !== 'LEADER') {
|
||||
return false;
|
||||
}
|
||||
if (user.hospitalId !== hospitalId || user.departmentId !== departmentId) {
|
||||
return false;
|
||||
}
|
||||
if (groupId == null) {
|
||||
return true;
|
||||
}
|
||||
return user.groupId == null || user.groupId === groupId;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const activeDepartmentId = currentDepartmentIdFromQuery.value || searchForm.departmentId;
|
||||
const activeHospitalId = currentHospitalIdFromQuery.value || searchForm.hospitalId;
|
||||
const activeDepartmentId =
|
||||
currentDepartmentIdFromQuery.value || searchForm.departmentId;
|
||||
const activeHospitalId =
|
||||
currentHospitalIdFromQuery.value || searchForm.hospitalId;
|
||||
const [groupRes, leaderRes] = await Promise.all([
|
||||
getGroups({
|
||||
page: page.value,
|
||||
@ -281,11 +440,17 @@ const clearDepartmentFilter = () => {
|
||||
const openCreateDialog = async () => {
|
||||
isEdit.value = false;
|
||||
currentId.value = null;
|
||||
form.hospitalId = userStore.role === 'SYSTEM_ADMIN' ? (currentHospitalIdFromQuery.value || searchForm.hospitalId || null) : userStore.userInfo?.hospitalId;
|
||||
form.hospitalId =
|
||||
userStore.role === 'SYSTEM_ADMIN'
|
||||
? currentHospitalIdFromQuery.value || searchForm.hospitalId || null
|
||||
: userStore.userInfo?.hospitalId;
|
||||
if (userStore.role === 'SYSTEM_ADMIN' && form.hospitalId) {
|
||||
await handleFormHospitalChange(form.hospitalId);
|
||||
}
|
||||
form.departmentId = currentDepartmentIdFromQuery.value || searchForm.departmentId || null;
|
||||
form.departmentId =
|
||||
currentDepartmentIdFromQuery.value || searchForm.departmentId || null;
|
||||
form.leaderUserId = null;
|
||||
await loadLeaderOptions(form.hospitalId, form.departmentId, null);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
@ -303,6 +468,8 @@ const openEditDialog = async (row) => {
|
||||
await handleFormHospitalChange(hospitalId);
|
||||
}
|
||||
form.departmentId = row.departmentId;
|
||||
form.leaderUserId = null;
|
||||
await loadLeaderOptions(form.hospitalId, form.departmentId, row.id);
|
||||
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
@ -314,7 +481,9 @@ const resetForm = () => {
|
||||
form.name = '';
|
||||
form.hospitalId = null;
|
||||
form.departmentId = null;
|
||||
form.leaderUserId = null;
|
||||
formDepartments.value = [];
|
||||
leaderOptions.value = [];
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -323,14 +492,42 @@ const handleSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let targetGroupId = null;
|
||||
let targetDepartmentId = form.departmentId;
|
||||
let targetHospitalId = form.hospitalId;
|
||||
if (isEdit.value) {
|
||||
// Backend patch dto might just accept name. Sending departmentId may not be allowed or needed.
|
||||
await updateGroup(currentId.value, { name: form.name });
|
||||
const updated = await updateGroup(currentId.value, { name: form.name });
|
||||
targetGroupId = updated?.id ?? currentId.value;
|
||||
targetDepartmentId = updated?.departmentId ?? form.departmentId;
|
||||
targetHospitalId = updated?.department?.hospitalId ?? form.hospitalId;
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createGroup({ name: form.name, departmentId: form.departmentId });
|
||||
const created = await createGroup({
|
||||
name: form.name,
|
||||
departmentId: form.departmentId,
|
||||
});
|
||||
targetGroupId = created?.id;
|
||||
targetDepartmentId = created?.departmentId ?? form.departmentId;
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
if (
|
||||
canAssignLeaderInDialog.value
|
||||
&& form.leaderUserId
|
||||
&& targetGroupId
|
||||
&& targetDepartmentId
|
||||
&& targetHospitalId
|
||||
) {
|
||||
await updateUser(form.leaderUserId, {
|
||||
role: 'LEADER',
|
||||
hospitalId: targetHospitalId,
|
||||
departmentId: targetDepartmentId,
|
||||
groupId: targetGroupId,
|
||||
});
|
||||
ElMessage.success('小组组长已设置');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -343,15 +540,12 @@ const handleSubmit = async () => {
|
||||
};
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除小组 "${row.name}" 吗?`,
|
||||
'警告',
|
||||
{
|
||||
ElMessageBox.confirm(`确定要删除小组 "${row.name}" 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await deleteGroup(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
@ -359,7 +553,8 @@ const handleDelete = (row) => {
|
||||
} catch (error) {
|
||||
console.error('Delete failed', error);
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// --- Lifecycle ---
|
||||
@ -382,7 +577,17 @@ watch(
|
||||
() => {
|
||||
page.value = 1;
|
||||
fetchData();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [form.hospitalId, form.departmentId],
|
||||
async ([hospitalId, departmentId]) => {
|
||||
if (!dialogVisible.value) {
|
||||
return;
|
||||
}
|
||||
await loadLeaderOptions(hospitalId, departmentId, isEdit.value ? currentId.value : null);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@ -19,6 +19,11 @@
|
||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="医院名称" min-width="200" />
|
||||
<el-table-column prop="adminDisplay" label="医院管理员" min-width="220">
|
||||
<template #default="{ row }">
|
||||
{{ row.adminDisplay || '未设置' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ new Date(row.createdAt).toLocaleString() }}
|
||||
@ -54,6 +59,22 @@
|
||||
<el-form-item label="医院名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入医院名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="医院管理员" v-if="userStore.role === 'SYSTEM_ADMIN'">
|
||||
<el-select
|
||||
v-model="form.adminUserId"
|
||||
placeholder="可选:选择后将任命为医院管理员"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in hospitalAdminOptions"
|
||||
:key="user.id"
|
||||
:label="`${user.name}(${user.phone} / ${getRoleName(user.role)})`"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@ -70,6 +91,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getHospitals, createHospital, updateHospital, deleteHospital } from '../../api/organization';
|
||||
import { getUsers, updateUser } from '../../api/users';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
const router = useRouter();
|
||||
@ -81,6 +103,18 @@ const tableData = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const hospitalAdminOptions = ref([]);
|
||||
|
||||
const roleMap = {
|
||||
SYSTEM_ADMIN: '系统管理员',
|
||||
HOSPITAL_ADMIN: '医院管理员',
|
||||
DIRECTOR: '科室主任',
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
ENGINEER: '工程师',
|
||||
};
|
||||
|
||||
const getRoleName = (role) => roleMap[role] || role;
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
@ -94,7 +128,8 @@ const formRef = ref(null);
|
||||
const currentId = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
name: ''
|
||||
name: '',
|
||||
adminUserId: null,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
@ -110,7 +145,29 @@ const fetchData = async () => {
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchForm.keyword || undefined
|
||||
});
|
||||
tableData.value = res.list || [];
|
||||
|
||||
let hospitalAdminNameMap = {};
|
||||
try {
|
||||
const userRes = await getUsers();
|
||||
const users = Array.isArray(userRes?.list) ? userRes.list : [];
|
||||
hospitalAdminNameMap = users.reduce((acc, user) => {
|
||||
if (user.role !== 'HOSPITAL_ADMIN' || !user.hospitalId) {
|
||||
return acc;
|
||||
}
|
||||
if (!acc[user.hospitalId]) {
|
||||
acc[user.hospitalId] = [];
|
||||
}
|
||||
acc[user.hospitalId].push(user.name || '-');
|
||||
return acc;
|
||||
}, {});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch hospital admins', error);
|
||||
}
|
||||
|
||||
tableData.value = (res.list || []).map((hospital) => ({
|
||||
...hospital,
|
||||
adminDisplay: (hospitalAdminNameMap[hospital.id] || []).join('、') || '未设置',
|
||||
}));
|
||||
total.value = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch hospitals', error);
|
||||
@ -128,6 +185,7 @@ const resetSearch = () => {
|
||||
const openCreateDialog = () => {
|
||||
isEdit.value = false;
|
||||
currentId.value = null;
|
||||
loadHospitalAdminOptions();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
@ -135,6 +193,7 @@ const openEditDialog = (row) => {
|
||||
isEdit.value = true;
|
||||
currentId.value = row.id;
|
||||
form.name = row.name;
|
||||
loadHospitalAdminOptions(row.id);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
@ -143,6 +202,27 @@ const resetForm = () => {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
form.name = '';
|
||||
form.adminUserId = null;
|
||||
};
|
||||
|
||||
const loadHospitalAdminOptions = async (hospitalId) => {
|
||||
if (userStore.role !== 'SYSTEM_ADMIN') {
|
||||
hospitalAdminOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const userRes = await getUsers();
|
||||
const users = Array.isArray(userRes?.list) ? userRes.list : [];
|
||||
hospitalAdminOptions.value = users.filter((user) => {
|
||||
// 仅允许选择“医院管理员”角色,避免误选普通人员。
|
||||
if (user.role !== 'HOSPITAL_ADMIN') {
|
||||
return false;
|
||||
}
|
||||
if (!hospitalId) {
|
||||
return true;
|
||||
}
|
||||
return user.hospitalId == null || user.hospitalId === hospitalId;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -151,13 +231,35 @@ const handleSubmit = async () => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let targetHospitalId = null;
|
||||
if (isEdit.value) {
|
||||
await updateHospital(currentId.value, form);
|
||||
const updated = await updateHospital(currentId.value, { name: form.name });
|
||||
targetHospitalId = updated?.id ?? currentId.value;
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createHospital(form);
|
||||
const created = await createHospital({ name: form.name });
|
||||
targetHospitalId = created?.id;
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
if (form.adminUserId && targetHospitalId) {
|
||||
const selectedAdmin = hospitalAdminOptions.value.find(
|
||||
(user) => user.id === form.adminUserId,
|
||||
);
|
||||
if (!selectedAdmin || selectedAdmin.role !== 'HOSPITAL_ADMIN') {
|
||||
ElMessage.error('仅可选择医院管理员角色人员');
|
||||
return;
|
||||
}
|
||||
|
||||
await updateUser(form.adminUserId, {
|
||||
role: 'HOSPITAL_ADMIN',
|
||||
hospitalId: targetHospitalId,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
});
|
||||
ElMessage.success('医院管理员已设置');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
|
||||
@ -33,7 +33,10 @@
|
||||
</div>
|
||||
<div class="node-text">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<span v-if="data.type === 'department'" class="node-sub-label">
|
||||
<span v-if="data.type === 'hospital'" class="node-sub-label">
|
||||
医院管理员:{{ data.adminDisplay || '未设置' }}
|
||||
</span>
|
||||
<span v-else-if="data.type === 'department'" class="node-sub-label">
|
||||
主任:{{ data.directorDisplay || '未设置' }}
|
||||
</span>
|
||||
<span v-else-if="data.type === 'group'" class="node-sub-label">
|
||||
@ -143,7 +146,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'department'">主任:{{ row.directorDisplay || '未设置' }}</span>
|
||||
<span v-if="row.type === 'hospital'">医院管理员:{{ row.adminDisplay || '未设置' }}</span>
|
||||
<span v-else-if="row.type === 'department'">主任:{{ row.directorDisplay || '未设置' }}</span>
|
||||
<span v-else-if="row.type === 'group'">组长:{{ row.leaderDisplay || '未设置' }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@ -282,7 +286,7 @@ const roleMap = {
|
||||
SYSTEM_ADMIN: '系统管理员',
|
||||
HOSPITAL_ADMIN: '医院管理员',
|
||||
DIRECTOR: '科室主任',
|
||||
LEADER: '医疗组长',
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
ENGINEER: '工程师'
|
||||
};
|
||||
@ -424,6 +428,9 @@ const activeNodeMeta = computed(() => {
|
||||
if (!activeNode.value) {
|
||||
return '';
|
||||
}
|
||||
if (activeNode.value.type === 'hospital') {
|
||||
return `当前医院管理员:${activeNode.value.adminDisplay || '未设置'}`;
|
||||
}
|
||||
if (activeNode.value.type === 'department') {
|
||||
return `当前科室主任:${activeNode.value.directorDisplay || '未设置'}`;
|
||||
}
|
||||
@ -478,7 +485,14 @@ const fetchTreeData = async () => {
|
||||
|
||||
const directorNameMap = {};
|
||||
const leaderNameMap = {};
|
||||
const hospitalAdminNameMap = {};
|
||||
users.forEach((user) => {
|
||||
if (user.role === 'HOSPITAL_ADMIN' && user.hospitalId) {
|
||||
if (!hospitalAdminNameMap[user.hospitalId]) {
|
||||
hospitalAdminNameMap[user.hospitalId] = [];
|
||||
}
|
||||
hospitalAdminNameMap[user.hospitalId].push(user.name);
|
||||
}
|
||||
if (user.role === 'DIRECTOR' && user.departmentId) {
|
||||
if (!directorNameMap[user.departmentId]) {
|
||||
directorNameMap[user.departmentId] = [];
|
||||
@ -495,6 +509,8 @@ const fetchTreeData = async () => {
|
||||
|
||||
const tree = hospitals.map(h => {
|
||||
const hDepts = departments.filter(d => d.hospitalId === h.id);
|
||||
const adminDisplay =
|
||||
(hospitalAdminNameMap[h.id] || []).join('、') || '未设置';
|
||||
|
||||
const deptNodes = hDepts.map(d => {
|
||||
const dGroups = groups.filter(g => g.departmentId === d.id);
|
||||
@ -535,7 +551,7 @@ const fetchTreeData = async () => {
|
||||
}));
|
||||
|
||||
return {
|
||||
key: `h_${h.id}`, id: h.id, name: h.name, type: 'hospital', children: [...deptNodes, ...hUserNodes]
|
||||
key: `h_${h.id}`, id: h.id, name: h.name, type: 'hospital', adminDisplay, children: [...deptNodes, ...hUserNodes]
|
||||
};
|
||||
});
|
||||
|
||||
@ -567,7 +583,7 @@ const openSetOwnerDialog = (node) => {
|
||||
ownerCandidates.value = allUsers.value.filter((user) =>
|
||||
user.hospitalId === node.hospitalId
|
||||
&& user.departmentId === node.id
|
||||
&& ['DIRECTOR', 'LEADER'].includes(user.role),
|
||||
&& user.role === 'DIRECTOR',
|
||||
);
|
||||
} else {
|
||||
ownerCandidates.value = allUsers.value.filter((user) =>
|
||||
|
||||
@ -29,13 +29,6 @@
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备 SN">
|
||||
<el-input
|
||||
v-model="searchForm.deviceSn"
|
||||
placeholder="按设备 SN 过滤"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" icon="Search">查询</el-button>
|
||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||
@ -56,27 +49,17 @@
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="140" />
|
||||
<el-table-column prop="idCardHash" label="证件哈希" min-width="200" />
|
||||
<el-table-column prop="idCardHash" label="身份证" min-width="200" />
|
||||
<el-table-column label="归属医院" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.hospital?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="归属人员" min-width="140">
|
||||
<el-table-column label="归属医生" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.doctor?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.devices?.length || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备 SN" min-width="220">
|
||||
<template #default="{ row }">
|
||||
{{ formatDeviceSn(row.devices) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="openRecordDialog(row)">
|
||||
@ -119,24 +102,21 @@
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="证件哈希" prop="idCardHash">
|
||||
<el-input v-model="form.idCardHash" placeholder="请输入证件哈希" />
|
||||
<el-form-item label="身份证" prop="idCardHash">
|
||||
<el-input v-model="form.idCardHash" placeholder="请输入身份证号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="归属人员" prop="doctorId">
|
||||
<el-select
|
||||
<el-form-item label="归属医生" prop="doctorId">
|
||||
<el-tree-select
|
||||
v-model="form.doctorId"
|
||||
:data="doctorTreeOptions"
|
||||
:props="doctorTreeProps"
|
||||
check-strictly
|
||||
filterable
|
||||
placeholder="请选择归属人员(医生/主任/组长)"
|
||||
clearable
|
||||
placeholder="请选择归属医生(按科室/小组)"
|
||||
style="width: 100%;"
|
||||
:disabled="userStore.role === 'DOCTOR'"
|
||||
>
|
||||
<el-option
|
||||
v-for="doctor in doctorOptions"
|
||||
:key="doctor.id"
|
||||
:label="`${doctor.name}(${getRoleName(doctor.role)} / ${doctor.phone})`"
|
||||
:value="doctor.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -153,7 +133,7 @@
|
||||
<el-descriptions :column="4" border class="mb-16">
|
||||
<el-descriptions-item label="患者">{{ currentPatientName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ recordSummary.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="证件哈希">{{ recordSummary.idCardHash || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证">{{ recordSummary.idCardHash || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="记录数">{{ recordList.length }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@ -199,7 +179,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { reactive, ref, onMounted, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
getPatients,
|
||||
@ -211,13 +191,14 @@ import {
|
||||
getPatientLifecycle,
|
||||
} from '../../api/patients';
|
||||
import { getHospitals } from '../../api/organization';
|
||||
import { getDepartments, getGroups } from '../../api/organization';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const roleMap = {
|
||||
DIRECTOR: '科室主任',
|
||||
LEADER: '医疗组长',
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
};
|
||||
|
||||
@ -230,10 +211,11 @@ const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const hospitals = ref([]);
|
||||
const departments = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
deviceSn: '',
|
||||
hospitalId: null,
|
||||
});
|
||||
|
||||
@ -270,26 +252,82 @@ const recordSummary = reactive({
|
||||
});
|
||||
const recordList = ref([]);
|
||||
|
||||
const formatDeviceSn = (devices = []) => {
|
||||
if (!Array.isArray(devices) || devices.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
return devices.map((item) => item.snCode).join(',');
|
||||
const doctorTreeProps = {
|
||||
value: 'value',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
|
||||
const departmentNameMap = computed(() => {
|
||||
return Object.fromEntries((departments.value || []).map((item) => [item.id, item.name]));
|
||||
});
|
||||
|
||||
const groupNameMap = computed(() => {
|
||||
return Object.fromEntries((groups.value || []).map((item) => [item.id, item.name]));
|
||||
});
|
||||
|
||||
const doctorTreeOptions = computed(() => {
|
||||
const options = Array.isArray(doctorOptions.value) ? doctorOptions.value : [];
|
||||
const deptMap = new Map();
|
||||
|
||||
options.forEach((doctor) => {
|
||||
const deptId = doctor.departmentId ?? 0;
|
||||
const groupId = doctor.groupId ?? 0;
|
||||
const deptKey = `dept_${deptId}`;
|
||||
const groupKey = `group_${groupId}`;
|
||||
|
||||
if (!deptMap.has(deptKey)) {
|
||||
const deptLabel = deptId
|
||||
? departmentNameMap.value[deptId] || `科室#${deptId}`
|
||||
: '未分配科室';
|
||||
deptMap.set(deptKey, {
|
||||
value: deptKey,
|
||||
label: deptLabel,
|
||||
disabled: true,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
const deptNode = deptMap.get(deptKey);
|
||||
|
||||
if (groupId) {
|
||||
let groupNode = deptNode.children.find((item) => item.value === groupKey);
|
||||
if (!groupNode) {
|
||||
groupNode = {
|
||||
value: groupKey,
|
||||
label: groupNameMap.value[groupId] || `小组#${groupId}`,
|
||||
disabled: true,
|
||||
children: [],
|
||||
};
|
||||
deptNode.children.push(groupNode);
|
||||
}
|
||||
|
||||
groupNode.children.push({
|
||||
value: doctor.id,
|
||||
label: `${doctor.name}(${getRoleName(doctor.role)} / ${doctor.phone})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
deptNode.children.push({
|
||||
value: doctor.id,
|
||||
label: `${doctor.name}(${getRoleName(doctor.role)} / ${doctor.phone})`,
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(deptMap.values()).sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-Hans-CN'));
|
||||
});
|
||||
|
||||
const applyFiltersAndPagination = () => {
|
||||
const keyword = searchForm.keyword.trim();
|
||||
const deviceSn = searchForm.deviceSn.trim();
|
||||
|
||||
const filtered = allPatients.value.filter((patient) => {
|
||||
const hitKeyword = !keyword
|
||||
|| patient.name?.includes(keyword)
|
||||
|| patient.phone?.includes(keyword);
|
||||
|
||||
const hitDevice = !deviceSn
|
||||
|| (patient.devices || []).some((device) => device.snCode?.includes(deviceSn));
|
||||
|
||||
return hitKeyword && hitDevice;
|
||||
return hitKeyword;
|
||||
});
|
||||
|
||||
total.value = filtered.length;
|
||||
@ -321,6 +359,25 @@ const fetchDoctorOptions = async () => {
|
||||
doctorOptions.value = Array.isArray(res) ? res : [];
|
||||
};
|
||||
|
||||
const fetchOrgNodesForDoctorTree = async () => {
|
||||
const params = { pageSize: 100 };
|
||||
if (userStore.role === 'SYSTEM_ADMIN') {
|
||||
if (!searchForm.hospitalId) {
|
||||
departments.value = [];
|
||||
groups.value = [];
|
||||
return;
|
||||
}
|
||||
params.hospitalId = searchForm.hospitalId;
|
||||
}
|
||||
|
||||
const [deptRes, groupRes] = await Promise.all([
|
||||
getDepartments(params),
|
||||
getGroups(params),
|
||||
]);
|
||||
departments.value = Array.isArray(deptRes?.list) ? deptRes.list : [];
|
||||
groups.value = Array.isArray(groupRes?.list) ? groupRes.list : [];
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if (userStore.role === 'SYSTEM_ADMIN' && !searchForm.hospitalId) {
|
||||
allPatients.value = [];
|
||||
@ -345,6 +402,7 @@ const fetchData = async () => {
|
||||
|
||||
const handleSearchHospitalChange = async () => {
|
||||
page.value = 1;
|
||||
await fetchOrgNodesForDoctorTree();
|
||||
await fetchDoctorOptions();
|
||||
await fetchData();
|
||||
};
|
||||
@ -356,7 +414,6 @@ const handleSearch = () => {
|
||||
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.deviceSn = '';
|
||||
page.value = 1;
|
||||
fetchData();
|
||||
};
|
||||
@ -373,6 +430,7 @@ const resetForm = () => {
|
||||
const openCreateDialog = async () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
await fetchOrgNodesForDoctorTree();
|
||||
await fetchDoctorOptions();
|
||||
|
||||
if (userStore.role === 'DOCTOR') {
|
||||
@ -383,6 +441,7 @@ const openCreateDialog = async () => {
|
||||
|
||||
const openEditDialog = async (row) => {
|
||||
isEdit.value = true;
|
||||
await fetchOrgNodesForDoctorTree();
|
||||
await fetchDoctorOptions();
|
||||
const detail = await getPatientById(row.id);
|
||||
currentEditId.value = detail.id;
|
||||
@ -468,6 +527,7 @@ const openRecordDialog = async (row) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchHospitalsForAdmin();
|
||||
await fetchOrgNodesForDoctorTree();
|
||||
await fetchDoctorOptions();
|
||||
await fetchData();
|
||||
});
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<el-option label="系统管理员" value="SYSTEM_ADMIN" />
|
||||
<el-option label="医院管理员" value="HOSPITAL_ADMIN" />
|
||||
<el-option label="科室主任" value="DIRECTOR" />
|
||||
<el-option label="医疗组长" value="LEADER" />
|
||||
<el-option label="小组组长" value="LEADER" />
|
||||
<el-option label="医生" value="DOCTOR" />
|
||||
<el-option label="工程师" value="ENGINEER" />
|
||||
</el-select>
|
||||
@ -127,7 +127,7 @@
|
||||
<el-option label="系统管理员" value="SYSTEM_ADMIN" />
|
||||
<el-option label="医院管理员" value="HOSPITAL_ADMIN" />
|
||||
<el-option label="科室主任" value="DIRECTOR" />
|
||||
<el-option label="医疗组长" value="LEADER" />
|
||||
<el-option label="小组组长" value="LEADER" />
|
||||
<el-option label="医生" value="DOCTOR" />
|
||||
<el-option label="工程师" value="ENGINEER" />
|
||||
</el-select>
|
||||
@ -318,7 +318,7 @@ const roleMap = {
|
||||
SYSTEM_ADMIN: '系统管理员',
|
||||
HOSPITAL_ADMIN: '医院管理员',
|
||||
DIRECTOR: '科室主任',
|
||||
LEADER: '医疗组长',
|
||||
LEADER: '小组组长',
|
||||
DOCTOR: '医生',
|
||||
ENGINEER: '工程师',
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user