Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -21,7 +21,7 @@ public final class ServiceException extends RuntimeException {
|
|||||||
/**
|
/**
|
||||||
* 错误明细,内部调试错误
|
* 错误明细,内部调试错误
|
||||||
*
|
*
|
||||||
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
|
* 和
|
||||||
*/
|
*/
|
||||||
private String detailMessage;
|
private String detailMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
<groupId>org.apache.velocity</groupId>
|
<groupId>org.apache.velocity</groupId>
|
||||||
<artifactId>velocity-engine-core</artifactId>
|
<artifactId>velocity-engine-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- rabbitMQ -->
|
<!-- rabbitMQ -->
|
||||||
<!-- <dependency>
|
<!-- <dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
package com.openhis.web.doctorstation.appservice;
|
package com.openhis.web.doctorstation.appservice;
|
||||||
|
|
||||||
import com.core.common.core.domain.R;
|
|
||||||
import com.openhis.template.domain.DoctorPhrase;
|
import com.openhis.template.domain.DoctorPhrase;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface IDoctorPhraseAppService {
|
public interface IDoctorPhraseAppService {
|
||||||
R<?> getDoctorPhraseList();
|
List<DoctorPhrase> getDoctorPhraseList();
|
||||||
|
List<DoctorPhrase> searchDoctorPhraseList(String phraseName, Integer phraseType);
|
||||||
|
Boolean addDoctorPhrase(DoctorPhrase doctorPhrase);
|
||||||
|
Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase);
|
||||||
|
Boolean deleteDoctorPhrase(Integer doctorPhraseId);
|
||||||
|
|
||||||
R<?> searchDoctorPhraseList(String phraseName ,Integer phraseType);
|
|
||||||
|
|
||||||
R<?> addDoctorPhrase(DoctorPhrase doctorPhrase);
|
|
||||||
|
|
||||||
R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase);
|
|
||||||
|
|
||||||
R<?> deleteDoctorPhrase(Integer doctorPhraseId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,71 @@ package com.openhis.web.doctorstation.appservice.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.core.common.core.domain.R;
|
import com.openhis.common.enums.BindingType;
|
||||||
import com.openhis.template.domain.DoctorPhrase;
|
import com.openhis.template.domain.DoctorPhrase;
|
||||||
import com.openhis.template.service.IDoctorPhraseService;
|
import com.openhis.template.service.IDoctorPhraseService;
|
||||||
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
|
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
|
public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IDoctorPhraseService doctorPhraseService;
|
private IDoctorPhraseService doctorPhraseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> getDoctorPhraseList() {
|
public List<DoctorPhrase> getDoctorPhraseList() {
|
||||||
List<DoctorPhrase> list = doctorPhraseService.list();
|
Long orgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
return R.ok(list);
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("orgId: {}", orgId);
|
||||||
}
|
}
|
||||||
|
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
// 1. 获取当前登录用户信息
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
// 2. 权限判定:非管理员才需要过滤
|
||||||
|
// 如果是超级管理员,默认可以看到所有
|
||||||
|
if (!SecurityUtils.isAdmin(userId)) {
|
||||||
|
// 3. 获取当前医生的科室编码
|
||||||
|
String deptCode = "";
|
||||||
|
if (orgId != null) {
|
||||||
|
deptCode = String.valueOf(orgId);
|
||||||
|
}
|
||||||
|
// final 变量用于 Lambda 表达式
|
||||||
|
String finalDeptCode = deptCode;
|
||||||
|
// 4. 核心逻辑:三级数据共享
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
|
||||||
|
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
|
||||||
|
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
|
||||||
|
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 5. 按排序号排序(可选优化,让常用语显示更整齐)
|
||||||
|
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> searchDoctorPhraseList(String phraseName,Integer phraseType) {
|
public List<DoctorPhrase> searchDoctorPhraseList(String phraseName,Integer phraseType) {
|
||||||
|
// 1. 获取当前登录用户信息
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
//2.获取到当前医生当前科室的id
|
||||||
|
Long orgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Search phrase - orgId: {}, phraseName: {}, phraseType: {}", orgId, phraseName, phraseType);
|
||||||
|
}
|
||||||
|
String deptCode = "";
|
||||||
|
if (orgId != null) {
|
||||||
|
deptCode = String.valueOf(orgId);
|
||||||
|
}
|
||||||
|
String finalDeptCode = deptCode;
|
||||||
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<DoctorPhrase> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (phraseName !=null && ObjectUtil.isNotEmpty(phraseName)) {
|
if (phraseName !=null && ObjectUtil.isNotEmpty(phraseName)) {
|
||||||
wrapper.like(DoctorPhrase::getPhraseName, phraseName);
|
wrapper.like(DoctorPhrase::getPhraseName, phraseName);
|
||||||
@@ -33,50 +74,135 @@ public class DoctorPhraseAppServiceImpl implements IDoctorPhraseAppService {
|
|||||||
if (phraseType !=null && ObjectUtil.isNotEmpty(phraseType)) {
|
if (phraseType !=null && ObjectUtil.isNotEmpty(phraseType)) {
|
||||||
wrapper.eq(DoctorPhrase::getPhraseType, phraseType);
|
wrapper.eq(DoctorPhrase::getPhraseType, phraseType);
|
||||||
}
|
}
|
||||||
|
Long currentUserId = SecurityUtils.getUserId();
|
||||||
|
if (!SecurityUtils.isAdmin(currentUserId)) {
|
||||||
|
// 建议统一使用 staffId 进行业务隔离
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.eq(DoctorPhrase::getStaffId, userId.intValue()) // 1. 个人的:只看 staffId 是我的
|
||||||
|
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.HOSPITAL.getValue())) // 2. 全院的:类型为 3
|
||||||
|
.or(o -> o.eq(DoctorPhrase::getPhraseType, BindingType.ORGANIZATION.getValue()) // 3. 科室的:类型为 2 且 科室编码匹配
|
||||||
|
.eq(DoctorPhrase::getDeptCode, finalDeptCode))
|
||||||
|
);
|
||||||
|
}
|
||||||
//2.查询
|
//2.查询
|
||||||
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
|
List<DoctorPhrase> list = doctorPhraseService.list(wrapper);
|
||||||
return R.ok(list);
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> addDoctorPhrase(DoctorPhrase doctorPhrase) {
|
public Boolean addDoctorPhrase(DoctorPhrase doctorPhrase) {
|
||||||
//1.数据校验
|
// 1. 基础校验
|
||||||
if(ObjectUtil.isEmpty(doctorPhrase)){
|
if (ObjectUtil.isEmpty(doctorPhrase) || ObjectUtil.isEmpty(doctorPhrase.getPhraseName())) {
|
||||||
return R.fail("医生常用语不能为空");
|
throw new IllegalArgumentException("新增失败:常用语名称不能为空");
|
||||||
}
|
}
|
||||||
//2.名称唯一性校验
|
|
||||||
LambdaUpdateWrapper<DoctorPhrase> wrapper = new LambdaUpdateWrapper<>();
|
Long currentUserId = SecurityUtils.getUserId();
|
||||||
wrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName());
|
|
||||||
DoctorPhrase one = doctorPhraseService.getOne(wrapper);
|
/*
|
||||||
if(ObjectUtil.isNotEmpty(one)){
|
* 如果前端没传类型,必须给个默认值(比如 1-个人)
|
||||||
return R.fail("该名称已经存在");
|
* 否则存成 NULL,查询列表时会被过滤掉,导致"新增了却看不见"
|
||||||
|
*/
|
||||||
|
if (doctorPhrase.getPhraseType() == null) {
|
||||||
|
doctorPhrase.setPhraseType(BindingType.PERSONAL.getValue());
|
||||||
}
|
}
|
||||||
//3.新增
|
|
||||||
boolean save = doctorPhraseService.save(doctorPhrase);
|
// 2. 注入归属信息
|
||||||
System.out.println(save);
|
doctorPhrase.setStaffId(currentUserId.intValue());
|
||||||
return R.ok(save);
|
doctorPhrase.setCreatorId(currentUserId.intValue());
|
||||||
|
|
||||||
|
// 注入科室 (处理 null 情况)
|
||||||
|
Long orgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
if (orgId != null) {
|
||||||
|
// 检查dept_code字段长度,避免数据库错误
|
||||||
|
String deptCode = String.valueOf(orgId);
|
||||||
|
if (deptCode.length() > 50) { // 假设字段长度为50,根据实际情况调整
|
||||||
|
// 如果超过字段长度限制,可以考虑截断或抛出有意义的错误
|
||||||
|
throw new IllegalArgumentException("科室ID过长,无法保存");
|
||||||
|
}
|
||||||
|
doctorPhrase.setDeptCode(deptCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========== 【修复点 2】查重范围限制在"个人" ===========
|
||||||
|
// 使用 QueryWrapper 而不是 UpdateWrapper
|
||||||
|
LambdaQueryWrapper<DoctorPhrase> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(DoctorPhrase::getPhraseName, doctorPhrase.getPhraseName())
|
||||||
|
.eq(DoctorPhrase::getStaffId, currentUserId.intValue()); // 重点:只查自己名下的!
|
||||||
|
|
||||||
|
if (doctorPhraseService.count(queryWrapper) > 0) {
|
||||||
|
throw new IllegalArgumentException("新增失败:您已存在同名的常用语");
|
||||||
|
}
|
||||||
|
|
||||||
|
return doctorPhraseService.save(doctorPhrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> updateDoctorPhrase(DoctorPhrase doctorPhrase) {
|
public Boolean updateDoctorPhrase(DoctorPhrase doctorPhrase) {
|
||||||
//1.数据校验
|
// 1. 基础校验
|
||||||
if(ObjectUtil.isEmpty(doctorPhrase)){
|
if (ObjectUtil.isEmpty(doctorPhrase) || doctorPhrase.getId() == null) {
|
||||||
return R.fail("医生常用语不能为空");
|
throw new IllegalArgumentException("修改失败:ID不能为空");
|
||||||
}
|
}
|
||||||
//2.更新
|
|
||||||
boolean updateById = doctorPhraseService.updateById(doctorPhrase);
|
// 2. 查旧数据
|
||||||
return R.ok(updateById);
|
DoctorPhrase original = doctorPhraseService.getById(doctorPhrase.getId());
|
||||||
|
if (original == null) {
|
||||||
|
throw new IllegalArgumentException("修改失败:该常用语不存在或已被删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long currentUserId = SecurityUtils.getUserId();
|
||||||
|
|
||||||
|
// 3. 【权限校验优化】
|
||||||
|
if (!SecurityUtils.isAdmin(currentUserId)) {
|
||||||
|
// 规则 A:严禁修改全院公共模板 (Type=3)
|
||||||
|
// 这一步是关键!之前你的代码漏了这里,所以普通医生可能改动全院模板
|
||||||
|
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
|
||||||
|
throw new SecurityException("无权操作:全院公共常用语仅限管理员修改");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规则 B:严禁修改他人的模板
|
||||||
|
if (!original.getStaffId().equals(currentUserId.intValue())) {
|
||||||
|
throw new SecurityException("无权操作:您只能修改自己创建的常用语");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 数据保护:防止篡改归属
|
||||||
|
doctorPhrase.setStaffId(original.getStaffId());
|
||||||
|
doctorPhrase.setCreatorId(original.getCreatorId());
|
||||||
|
Long orgId = SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
if (orgId != null) {
|
||||||
|
// 检查dept_code字段长度,避免数据库错误
|
||||||
|
String deptCode = String.valueOf(orgId);
|
||||||
|
if (deptCode.length() > 50) { // 假设字段长度为50,根据实际情况调整
|
||||||
|
throw new IllegalArgumentException("科室ID过长,无法保存");
|
||||||
|
}
|
||||||
|
doctorPhrase.setDeptCode(deptCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doctorPhraseService.updateById(doctorPhrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> deleteDoctorPhrase(Integer doctorPhraseId) {
|
public Boolean deleteDoctorPhrase(Integer doctorPhraseId) {
|
||||||
//1.数据校验
|
if (doctorPhraseId == null) {
|
||||||
if(doctorPhraseId == null){
|
throw new IllegalArgumentException("删除失败:ID不能为空");
|
||||||
return R.fail("ID不能为空");
|
|
||||||
}
|
}
|
||||||
//2.删除
|
|
||||||
boolean removeById = doctorPhraseService.removeById(doctorPhraseId);
|
DoctorPhrase original = doctorPhraseService.getById(doctorPhraseId);
|
||||||
return R.ok(removeById);
|
if (original == null) {
|
||||||
|
throw new IllegalArgumentException("删除失败:数据不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long currentUserId = SecurityUtils.getUserId();
|
||||||
|
|
||||||
|
// 权限校验
|
||||||
|
if (!SecurityUtils.isAdmin(currentUserId)) {
|
||||||
|
if (BindingType.HOSPITAL.getValue().equals(original.getPhraseType())) {
|
||||||
|
throw new SecurityException("无权操作:全院公共常用语仅限管理员删除");
|
||||||
|
}
|
||||||
|
if (!original.getStaffId().equals(currentUserId.intValue())) {
|
||||||
|
throw new SecurityException("无权操作:您只能删除自己创建的常用语");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doctorPhraseService.removeById(doctorPhraseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
|||||||
e.setAge(e.getBirthDate() != null ? AgeCalculatorUtil.getAge(e.getBirthDate()) : "");
|
e.setAge(e.getBirthDate() != null ? AgeCalculatorUtil.getAge(e.getBirthDate()) : "");
|
||||||
// 就诊状态
|
// 就诊状态
|
||||||
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(EncounterStatus.class, e.getStatusEnum()));
|
e.setStatusEnum_enumText(EnumUtils.getInfoByValue(EncounterStatus.class, e.getStatusEnum()));
|
||||||
|
// 初复诊
|
||||||
|
e.setFirstEnum_enumText(EnumUtils.getInfoByValue(EncounterType.class, e.getFirstEnum()));
|
||||||
});
|
});
|
||||||
return patientInfo;
|
return patientInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openhis.web.doctorstation.controller;
|
package com.openhis.web.doctorstation.controller;
|
||||||
|
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.common.enums.BindingType;
|
||||||
import com.openhis.template.domain.DoctorPhrase;
|
import com.openhis.template.domain.DoctorPhrase;
|
||||||
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
|
import com.openhis.web.doctorstation.appservice.IDoctorPhraseAppService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -23,7 +24,13 @@ public class DoctorPhraseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public R<?> getDoctorPhraseList(){
|
public R<?> getDoctorPhraseList(){
|
||||||
|
try {
|
||||||
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
|
return R.ok(doctorPhraseAppService.getDoctorPhraseList());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 系统异常,使用error级别日志
|
||||||
|
log.error("获取医生常用语列表系统异常", e);
|
||||||
|
return R.fail("获取医生常用语列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +44,13 @@ public class DoctorPhraseController {
|
|||||||
@RequestParam(required = false) Integer phraseType,
|
@RequestParam(required = false) Integer phraseType,
|
||||||
@RequestParam(required = false) String phraseName
|
@RequestParam(required = false) String phraseName
|
||||||
){
|
){
|
||||||
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName,phraseType));
|
try {
|
||||||
|
return R.ok(doctorPhraseAppService.searchDoctorPhraseList(phraseName, phraseType));
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 系统异常,使用error级别日志
|
||||||
|
log.error("查询医生常用语系统异常", e);
|
||||||
|
return R.fail("查询医生常用语失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +61,22 @@ public class DoctorPhraseController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
public R<?> addDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
|
public R<?> addDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
|
||||||
return R.ok(doctorPhraseAppService.addDoctorPhrase(doctorPhrase));
|
try {
|
||||||
|
Boolean result = doctorPhraseAppService.addDoctorPhrase(doctorPhrase);
|
||||||
|
if (result != null && result) {
|
||||||
|
return R.ok("新增成功");
|
||||||
|
} else {
|
||||||
|
return R.fail("新增失败");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 参数错误异常,使用warn级别日志
|
||||||
|
log.warn("新增医生常用语参数错误: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 系统异常,使用error级别日志
|
||||||
|
log.error("新增医生常用语系统异常", e);
|
||||||
|
return R.fail("新增失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +87,26 @@ public class DoctorPhraseController {
|
|||||||
*/
|
*/
|
||||||
@PutMapping("/update")
|
@PutMapping("/update")
|
||||||
public R<?> updateDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
|
public R<?> updateDoctorPhrase(@RequestBody DoctorPhrase doctorPhrase){
|
||||||
return R.ok(doctorPhraseAppService.updateDoctorPhrase(doctorPhrase));
|
try {
|
||||||
|
Boolean result = doctorPhraseAppService.updateDoctorPhrase(doctorPhrase);
|
||||||
|
if (result != null && result) {
|
||||||
|
return R.ok("更新成功");
|
||||||
|
} else {
|
||||||
|
return R.fail("更新失败");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 参数错误异常,使用warn级别日志
|
||||||
|
log.warn("更新医生常用语参数错误: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
// 权限相关异常,使用warn级别日志
|
||||||
|
log.warn("更新医生常用语权限异常: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 系统异常,使用error级别日志
|
||||||
|
log.error("更新医生常用语系统异常", e);
|
||||||
|
return R.fail("更新失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +117,26 @@ public class DoctorPhraseController {
|
|||||||
*/
|
*/
|
||||||
@DeleteMapping("/delete/{DoctorPhraseId}")
|
@DeleteMapping("/delete/{DoctorPhraseId}")
|
||||||
public R<?> deleteDoctorPhrase(@PathVariable Integer DoctorPhraseId){
|
public R<?> deleteDoctorPhrase(@PathVariable Integer DoctorPhraseId){
|
||||||
return R.ok(doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId));
|
try {
|
||||||
|
Boolean result = doctorPhraseAppService.deleteDoctorPhrase(DoctorPhraseId);
|
||||||
|
if (result != null && result) {
|
||||||
|
return R.ok("删除成功");
|
||||||
|
} else {
|
||||||
|
return R.fail("删除失败");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 参数错误异常,使用warn级别日志
|
||||||
|
log.warn("删除医生常用语参数错误: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
// 权限相关异常,使用warn级别日志
|
||||||
|
log.warn("删除医生常用语权限异常: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 系统异常,使用error级别日志
|
||||||
|
log.error("删除医生常用语系统异常", e);
|
||||||
|
return R.fail("删除失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,4 +133,10 @@ public class PatientInfoDto {
|
|||||||
* 过号时间
|
* 过号时间
|
||||||
*/
|
*/
|
||||||
private Date missedTime;
|
private Date missedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初复诊标识
|
||||||
|
*/
|
||||||
|
private Integer firstEnum;
|
||||||
|
private String firstEnum_enumText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openhis.web.triageandqueuemanage.appservice;
|
package com.openhis.web.triageandqueuemanage.appservice;
|
||||||
|
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
@@ -22,6 +23,9 @@ public interface TriageQueueAppService {
|
|||||||
R<?> skip(TriageQueueActionReq req);
|
R<?> skip(TriageQueueActionReq req);
|
||||||
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
|
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
|
||||||
R<?> next(TriageQueueActionReq req);
|
R<?> next(TriageQueueActionReq req);
|
||||||
|
|
||||||
|
/** 叫号显示屏:获取当前叫号和等候队列信息 */
|
||||||
|
CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,19 +10,20 @@ import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
|||||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
|
||||||
|
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
||||||
@@ -35,6 +36,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TriageQueueItemService triageQueueItemService;
|
private TriageQueueItemService triageQueueItemService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CallNumberSseManager callNumberSseManager;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private TriageCandidateExclusionService triageCandidateExclusionService;
|
private TriageCandidateExclusionService triageCandidateExclusionService;
|
||||||
|
|
||||||
@@ -121,6 +125,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.setPatientName(it.getPatientName())
|
.setPatientName(it.getPatientName())
|
||||||
.setHealthcareName(it.getHealthcareName())
|
.setHealthcareName(it.getHealthcareName())
|
||||||
.setPractitionerName(it.getPractitionerName())
|
.setPractitionerName(it.getPractitionerName())
|
||||||
|
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
|
||||||
|
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
||||||
.setStatus(STATUS_WAITING)
|
.setStatus(STATUS_WAITING)
|
||||||
.setQueueOrder(++maxOrder)
|
.setQueueOrder(++maxOrder)
|
||||||
.setDeleteFlag("0")
|
.setDeleteFlag("0")
|
||||||
@@ -238,6 +244,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
if (STATUS_WAITING.equals(selected.getStatus())) {
|
if (STATUS_WAITING.equals(selected.getStatus())) {
|
||||||
selected.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
selected.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
||||||
triageQueueItemService.updateById(selected);
|
triageQueueItemService.updateById(selected);
|
||||||
|
|
||||||
|
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
|
||||||
|
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
||||||
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
||||||
@@ -321,6 +331,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
|
|
||||||
|
// 完成后推送 SSE 消息(实时通知显示屏刷新)
|
||||||
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +427,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
triageQueueItemService.updateById(next);
|
triageQueueItemService.updateById(next);
|
||||||
|
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
|
|
||||||
|
// ✅ 过号重排后推送 SSE 消息(实时通知显示屏刷新)
|
||||||
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +569,179 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取叫号显示屏数据
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param date 日期
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return 显示屏数据
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId) {
|
||||||
|
// 如果没有传入租户ID,尝试从登录用户获取,否则默认为1
|
||||||
|
if (tenantId == null) {
|
||||||
|
try {
|
||||||
|
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
tenantId = 1; // 默认租户ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocalDate qd = date != null ? date : LocalDate.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有队列项(WAITING 和 CALLING 状态)某天的某个科室的某个状态
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
List<TriageQueueItem> allItems = triageQueueItemService.list(
|
||||||
|
new LambdaQueryWrapper<TriageQueueItem>()
|
||||||
|
.eq(TriageQueueItem::getQueueDate, qd)
|
||||||
|
.eq(TriageQueueItem::getOrganizationId, organizationId)
|
||||||
|
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||||
|
.in(TriageQueueItem::getStatus, STATUS_WAITING, STATUS_CALLING)
|
||||||
|
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||||
|
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||||
|
);
|
||||||
|
|
||||||
|
CallNumberDisplayResp resp = new CallNumberDisplayResp();
|
||||||
|
|
||||||
|
// 1. 获取科室名称(从第一条数据中取)
|
||||||
|
if (!allItems.isEmpty()) {
|
||||||
|
resp.setDepartmentName(allItems.get(0).getOrganizationName() + " 叫号显示屏");
|
||||||
|
} else {
|
||||||
|
resp.setDepartmentName("叫号显示屏");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找当前叫号中的患者(CALLING 状态)
|
||||||
|
TriageQueueItem callingItem = allItems.stream()
|
||||||
|
.filter(item -> STATUS_CALLING.equals(item.getStatus()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (callingItem != null) {
|
||||||
|
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||||
|
currentCall.setNumber(callingItem.getQueueOrder());
|
||||||
|
currentCall.setName(maskPatientName(callingItem.getPatientName()));
|
||||||
|
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
|
||||||
|
currentCall.setDoctor(callingItem.getPractitionerName());
|
||||||
|
resp.setCurrentCall(currentCall);
|
||||||
|
} else {
|
||||||
|
// 没有叫号中的患者,返回默认值
|
||||||
|
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||||
|
currentCall.setNumber(null);
|
||||||
|
currentCall.setName("-");
|
||||||
|
currentCall.setRoom("-");
|
||||||
|
currentCall.setDoctor("-");
|
||||||
|
resp.setCurrentCall(currentCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 按医生分组(包括 CALLING 和 WAITING 状态)
|
||||||
|
Map<Long, List<TriageQueueItem>> groupedByDoctor = allItems.stream()
|
||||||
|
// 严格按医生分组:仅保留有 practitionerId 的记录
|
||||||
|
.filter(item -> item.getPractitionerId() != null)
|
||||||
|
.collect(Collectors.groupingBy(TriageQueueItem::getPractitionerId));
|
||||||
|
|
||||||
|
// 每个医生的等待队列
|
||||||
|
List<CallNumberDisplayResp.DoctorGroup> waitingList = new ArrayList<>();
|
||||||
|
int totalWaiting = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<Long, List<TriageQueueItem>> entry : groupedByDoctor.entrySet()) {
|
||||||
|
|
||||||
|
List<TriageQueueItem> doctorItems = entry.getValue();
|
||||||
|
String doctorName = doctorItems.get(0).getPractitionerName();
|
||||||
|
if (doctorName == null || doctorName.isEmpty()) {
|
||||||
|
doctorName = "未分配";
|
||||||
|
}
|
||||||
|
// 按排队顺序排序
|
||||||
|
doctorItems.sort(Comparator.comparing(TriageQueueItem::getQueueOrder));
|
||||||
|
|
||||||
|
// 该医生 下边的患者列表 和 诊室号
|
||||||
|
CallNumberDisplayResp.DoctorGroup doctorGroup = new CallNumberDisplayResp.DoctorGroup();
|
||||||
|
doctorGroup.setDoctorName(doctorName);
|
||||||
|
|
||||||
|
// 获取诊室号(从该医生的任一患者中取)
|
||||||
|
String roomNo = doctorItems.stream()
|
||||||
|
.map(TriageQueueItem::getRoomNo)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("1号");
|
||||||
|
doctorGroup.setRoomNo(roomNo);
|
||||||
|
|
||||||
|
// 转换患者列表
|
||||||
|
List<CallNumberDisplayResp.PatientInfo> patients = new ArrayList<>();
|
||||||
|
for (TriageQueueItem item : doctorItems) {
|
||||||
|
CallNumberDisplayResp.PatientInfo patient = new CallNumberDisplayResp.PatientInfo();
|
||||||
|
patient.setId(item.getId());
|
||||||
|
patient.setName(maskPatientName(item.getPatientName()));
|
||||||
|
patient.setStatus(item.getStatus());
|
||||||
|
patient.setQueueOrder(item.getQueueOrder());
|
||||||
|
patients.add(patient);
|
||||||
|
|
||||||
|
// 统计等待人数(不包括 CALLING 状态)
|
||||||
|
if (STATUS_WAITING.equals(item.getStatus())) {
|
||||||
|
totalWaiting++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doctorGroup.setPatients(patients);
|
||||||
|
waitingList.add(doctorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 按医生名称排序
|
||||||
|
waitingList.sort(Comparator.comparing(CallNumberDisplayResp.DoctorGroup::getDoctorName));
|
||||||
|
|
||||||
|
resp.setWaitingList(waitingList);
|
||||||
|
resp.setWaitingCount(totalWaiting);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 患者姓名脱敏处理
|
||||||
|
* @param name 原始姓名
|
||||||
|
* @return 脱敏后的姓名(如:张*三)
|
||||||
|
*/
|
||||||
|
private String maskPatientName(String name) {
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
if (name.length() == 1) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (name.length() == 2) {
|
||||||
|
return name.charAt(0) + "*";
|
||||||
|
}
|
||||||
|
// 3个字及以上:保留首尾,中间用*代替
|
||||||
|
return name.charAt(0) + "*" + name.charAt(name.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送显示屏更新消息到 SSE
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param queueDate 队列日期
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
*/
|
||||||
|
private void pushDisplayUpdate(Long organizationId, LocalDate queueDate, Integer tenantId) {
|
||||||
|
try {
|
||||||
|
// 获取最新的显示屏数据
|
||||||
|
CallNumberDisplayResp displayData = getDisplayData(organizationId, queueDate, tenantId);
|
||||||
|
|
||||||
|
// 构造推送消息
|
||||||
|
Map<String, Object> message = new HashMap<>();
|
||||||
|
message.put("type", "update");
|
||||||
|
message.put("action", "queue_changed");
|
||||||
|
message.put("data", displayData);
|
||||||
|
message.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 推送到该科室的所有 SSE 连接
|
||||||
|
callNumberSseManager.pushToOrganization(organizationId, message);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// SSE 推送失败不应该影响业务逻辑
|
||||||
|
System.err.println("推送显示屏更新失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
package com.openhis.web.triageandqueuemanage.controller;
|
package com.openhis.web.triageandqueuemanage.controller;
|
||||||
|
|
||||||
|
import com.core.common.annotation.Anonymous;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
|
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -20,6 +29,9 @@ public class TriageQueueController {
|
|||||||
@Resource
|
@Resource
|
||||||
private TriageQueueAppService triageQueueAppService;
|
private TriageQueueAppService triageQueueAppService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CallNumberSseManager callNumberSseManager;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public R<?> list(@RequestParam(value = "organizationId", required = false) Long organizationId,
|
public R<?> list(@RequestParam(value = "organizationId", required = false) Long organizationId,
|
||||||
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||||
@@ -65,6 +77,100 @@ public class TriageQueueController {
|
|||||||
public R<?> next(@RequestBody(required = false) TriageQueueActionReq req) {
|
public R<?> next(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||||
return triageQueueAppService.next(req);
|
return triageQueueAppService.next(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号显示屏:获取当前叫号和等候队列信息
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param date 日期(可选,默认今天)
|
||||||
|
* @param tenantId 租户ID(可选,默认1)
|
||||||
|
* @return 显示屏数据
|
||||||
|
*/
|
||||||
|
@Anonymous // 显示屏不需要登录
|
||||||
|
@GetMapping("/display")
|
||||||
|
public R<CallNumberDisplayResp> getDisplayData(
|
||||||
|
@RequestParam(required = false) String organizationId,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
||||||
|
@RequestParam(required = false) Integer tenantId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Long orgId = resolveOrganizationId(organizationId);
|
||||||
|
if (orgId == null) {
|
||||||
|
return R.fail("organizationId参数不合法或未获取到登录用户科室");
|
||||||
|
}
|
||||||
|
Integer actualTenantId = resolveTenantId(tenantId);
|
||||||
|
CallNumberDisplayResp data = triageQueueAppService.getDisplayData(orgId, date, actualTenantId);
|
||||||
|
return R.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取显示屏数据失败", e);
|
||||||
|
return R.fail("获取显示屏数据失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号显示屏:SSE 实时推送
|
||||||
|
*/
|
||||||
|
@Anonymous
|
||||||
|
@GetMapping(value = "/display/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter streamDisplayData(
|
||||||
|
@RequestParam(required = false) String organizationId,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
||||||
|
@RequestParam(required = false) Integer tenantId
|
||||||
|
) {
|
||||||
|
// 1) 解析科室与租户(SSE 连接根据科室分组管理)
|
||||||
|
Long orgId = resolveOrganizationId(organizationId);
|
||||||
|
if (orgId == null) {
|
||||||
|
SseEmitter emitter = new SseEmitter(0L);
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("type", "error");
|
||||||
|
error.put("message", "organizationId参数不合法或未获取到登录用户科室");
|
||||||
|
callNumberSseManager.sendToEmitter(emitter, error);
|
||||||
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
Integer actualTenantId = resolveTenantId(tenantId);
|
||||||
|
// 2) 创建并注册 SSE 连接
|
||||||
|
SseEmitter emitter = callNumberSseManager.addEmitter(orgId);
|
||||||
|
try {
|
||||||
|
// 3) 连接建立后,先推送一次初始化数据
|
||||||
|
CallNumberDisplayResp data = triageQueueAppService.getDisplayData(orgId, date, actualTenantId);
|
||||||
|
Map<String, Object> init = new HashMap<>();
|
||||||
|
init.put("type", "init");
|
||||||
|
init.put("data", data);
|
||||||
|
init.put("timestamp", System.currentTimeMillis());
|
||||||
|
callNumberSseManager.sendToEmitter(emitter, init);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("SSE初始化数据发送失败", e);
|
||||||
|
}
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveOrganizationId(String organizationId) {
|
||||||
|
if (!StringUtils.hasText(organizationId)) {
|
||||||
|
try {
|
||||||
|
return SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(organizationId.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("非法organizationId: {}", organizationId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer resolveTenantId(Integer tenantId) {
|
||||||
|
if (tenantId != null) {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Integer loginTenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
return loginTenantId != null ? loginTenantId : 1;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.openhis.web.triageandqueuemanage.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号显示屏响应DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CallNumberDisplayResp {
|
||||||
|
/** 科室名称 */
|
||||||
|
private String departmentName;
|
||||||
|
|
||||||
|
/** 当前叫号信息 */
|
||||||
|
private CurrentCallInfo currentCall;
|
||||||
|
|
||||||
|
/** 等候患者列表(按医生分组) */
|
||||||
|
private List<DoctorGroup> waitingList;
|
||||||
|
|
||||||
|
/** 等待总人数 */
|
||||||
|
private Integer waitingCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前叫号信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class CurrentCallInfo {
|
||||||
|
/** 排队号 */
|
||||||
|
private Integer number;
|
||||||
|
/** 患者姓名(脱敏) */
|
||||||
|
private String name;
|
||||||
|
/** 诊室号 */
|
||||||
|
private String room;
|
||||||
|
/** 医生姓名 */
|
||||||
|
private String doctor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医生分组信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class DoctorGroup {
|
||||||
|
/** 医生姓名 */
|
||||||
|
private String doctorName;
|
||||||
|
/** 诊室号 */
|
||||||
|
private String roomNo;
|
||||||
|
/** 该医生的患者列表 */
|
||||||
|
private List<PatientInfo> patients;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 患者信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class PatientInfo {
|
||||||
|
/** 队列项ID */
|
||||||
|
private Long id;
|
||||||
|
/** 患者姓名(脱敏) */
|
||||||
|
private String name;
|
||||||
|
/** 状态:CALLING=就诊中,WAITING=等待 */
|
||||||
|
private String status;
|
||||||
|
/** 排队号 */
|
||||||
|
private Integer queueOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,12 @@ public class TriageQueueEncounterItem {
|
|||||||
private String patientName;
|
private String patientName;
|
||||||
private String healthcareName;
|
private String healthcareName;
|
||||||
private String practitionerName;
|
private String practitionerName;
|
||||||
|
|
||||||
|
// ========== 新增字段(可选,用于叫号显示屏)==========
|
||||||
|
/** 医生ID(可选) */
|
||||||
|
private Long practitionerId;
|
||||||
|
/** 诊室号(可选) */
|
||||||
|
private String roomNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.openhis.web.triageandqueuemanage.sse;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号显示屏 SSE 管理器(服务端推送)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class CallNumberSseManager {
|
||||||
|
|
||||||
|
private static final long NO_TIMEOUT = 0L; // 0 表示“永不超时”
|
||||||
|
// 按科室分组保存连接(消化内科有3个屏、心内科有2个屏)
|
||||||
|
// 很多屏幕同时连、同时断。故用 ConcurrentHashMap 存储,线程安全。内部分段锁,不阻塞其他科室的操作。
|
||||||
|
private static final Map<Long, CopyOnWriteArraySet<SseEmitter>> emitterMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并注册一个 SSE 连接(按科室分组保存)
|
||||||
|
*/
|
||||||
|
public SseEmitter addEmitter(Long organizationId) {
|
||||||
|
SseEmitter emitter = new SseEmitter(NO_TIMEOUT);
|
||||||
|
emitterMap.computeIfAbsent(organizationId, k -> new CopyOnWriteArraySet<>()).add(emitter);
|
||||||
|
|
||||||
|
emitter.onCompletion(() -> removeEmitter(organizationId, emitter));
|
||||||
|
emitter.onTimeout(() -> removeEmitter(organizationId, emitter));
|
||||||
|
emitter.onError((ex) -> removeEmitter(organizationId, emitter));
|
||||||
|
|
||||||
|
log.info("SSE连接建立:科室ID={}, 当前该科室连接数={}",
|
||||||
|
organizationId, emitterMap.get(organizationId).size());
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定科室的所有 SSE 连接推送消息
|
||||||
|
*/
|
||||||
|
public void pushToOrganization(Long organizationId, Object message) {
|
||||||
|
CopyOnWriteArraySet<SseEmitter> emitters = emitterMap.get(organizationId);
|
||||||
|
if (emitters == null || emitters.isEmpty()) {
|
||||||
|
log.debug("科室{}没有SSE连接,跳过推送", organizationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (SseEmitter emitter : emitters) {
|
||||||
|
if (!sendToEmitter(emitter, message)) {
|
||||||
|
removeEmitter(organizationId, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向单个 SSE 连接发送数据
|
||||||
|
*/
|
||||||
|
public boolean sendToEmitter(SseEmitter emitter, Object data) {
|
||||||
|
try {
|
||||||
|
emitter.send(SseEmitter.event().data(data));
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("SSE推送失败:{}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开或异常时移除 SSE 连接
|
||||||
|
*/
|
||||||
|
private void removeEmitter(Long organizationId, SseEmitter emitter) {
|
||||||
|
CopyOnWriteArraySet<SseEmitter> emitters = emitterMap.get(organizationId);
|
||||||
|
if (emitters != null) {
|
||||||
|
emitters.remove(emitter);
|
||||||
|
if (emitters.isEmpty()) {
|
||||||
|
emitterMap.remove(organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,9 @@
|
|||||||
T10.jz_practitioner_user_id,
|
T10.jz_practitioner_user_id,
|
||||||
T10.bus_no,
|
T10.bus_no,
|
||||||
T10.identifier_no,
|
T10.identifier_no,
|
||||||
T10.missed_time
|
T10.missed_time,
|
||||||
|
T10.first_enum,
|
||||||
|
T10.first_enum_enumText
|
||||||
from
|
from
|
||||||
(
|
(
|
||||||
SELECT T1.tenant_id AS tenant_id,
|
SELECT T1.tenant_id AS tenant_id,
|
||||||
@@ -52,7 +54,13 @@
|
|||||||
T1.organization_id AS org_id,
|
T1.organization_id AS org_id,
|
||||||
T8.bus_no AS bus_no,
|
T8.bus_no AS bus_no,
|
||||||
T9.identifier_no AS identifier_no,
|
T9.identifier_no AS identifier_no,
|
||||||
T1.missed_time AS missed_time
|
T1.missed_time AS missed_time,
|
||||||
|
T1.first_enum AS first_enum,
|
||||||
|
CASE
|
||||||
|
WHEN T1.first_enum = 1 THEN '初诊'
|
||||||
|
WHEN T1.first_enum = 2 THEN '复诊'
|
||||||
|
ELSE NULL
|
||||||
|
END AS first_enum_enumText
|
||||||
FROM adm_encounter AS T1
|
FROM adm_encounter AS T1
|
||||||
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
|
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
|
||||||
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
|
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
|
||||||
|
|||||||
@@ -18,21 +18,23 @@ public class TriageQueueItem {
|
|||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private Integer tenantId;
|
private Integer tenantId; // 租户ID
|
||||||
private LocalDate queueDate;
|
private LocalDate queueDate; // 队列日期
|
||||||
private Long organizationId;
|
private Long organizationId; // 科室ID
|
||||||
private String organizationName;
|
private String organizationName; // 科室名称
|
||||||
|
|
||||||
private Long encounterId;
|
private Long encounterId;
|
||||||
private Long patientId;
|
private Long patientId; // 患者ID
|
||||||
private String patientName;
|
private String patientName; // 患者姓名(脱敏)
|
||||||
|
|
||||||
private String healthcareName;
|
private String healthcareName; // 挂号类型(普通/专家)
|
||||||
private String practitionerName;
|
private String practitionerName; // 医生姓名
|
||||||
|
private Long practitionerId; // 医生ID(新增字段)
|
||||||
|
private String roomNo; // 诊室号(新增字段)
|
||||||
|
|
||||||
/** WAITING / CALLING / SKIPPED / COMPLETED */
|
/** WAITING / CALLING / SKIPPED / COMPLETED */
|
||||||
private String status;
|
private String status;
|
||||||
private Integer queueOrder;
|
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="Search"
|
||||||
/>
|
/>
|
||||||
<el-button class="icon-btn" circle @click="handleRefresh" type="text" plain>
|
<el-button class="icon-btn" circle @click="handleRefresh" link plain>
|
||||||
<el-icon icon-class="Refresh" size="24" :class="{ 'is-rotating': refreshing }">
|
<el-icon icon-class="Refresh" size="24" :class="{ 'is-rotating': refreshing }">
|
||||||
<Refresh />
|
<Refresh />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<!-- 第1行:姓名 -->
|
<!-- 第1行:姓名 -->
|
||||||
<div class="info-row name-row">
|
<div class="info-row name-row">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<el-text :text="item.patientName" tclass="name" width="auto">
|
<el-text :text="item.patientName" class="name" :truncated="true">
|
||||||
{{ item.patientName || '-' }}
|
{{ item.patientName || '-' }}
|
||||||
</el-text>
|
</el-text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<el-form-item v-if="showDefaultButtons" style="margin-left: 20px">
|
<el-form-item v-if="showDefaultButtons" style="margin-left: 20px">
|
||||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||||
<el-button v-if="needCollapse" type="text" @click="toggleExpand" style="margin-left: 16px">
|
<el-button v-if="needCollapse" link @click="toggleExpand" style="margin-left: 16px">
|
||||||
{{ isExpanded ? '收起' : '展开' }}
|
{{ isExpanded ? '收起' : '展开' }}
|
||||||
<el-icon class="el-icon--right">
|
<el-icon class="el-icon--right">
|
||||||
<DArrowLeft v-if="isExpanded" class="collapse-arrow collapse-arrow--up" />
|
<DArrowLeft v-if="isExpanded" class="collapse-arrow collapse-arrow--up" />
|
||||||
|
|||||||
@@ -565,6 +565,26 @@ const rules = reactive({
|
|||||||
chiefComplaint: [{ required: true, message: '请填写主诉', trigger: ['blur', 'submit'] }],
|
chiefComplaint: [{ required: true, message: '请填写主诉', trigger: ['blur', 'submit'] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听 patientInfo 变化,更新表单数据
|
||||||
|
watch(
|
||||||
|
() => props.patientInfo,
|
||||||
|
(newPatientInfo) => {
|
||||||
|
if (newPatientInfo) {
|
||||||
|
// 更新基础信息
|
||||||
|
formData.patientName = newPatientInfo.patientName || newPatientInfo.name || '';
|
||||||
|
formData.hospitalNo = newPatientInfo.busNo || newPatientInfo.hospitalNo || '';
|
||||||
|
formData.gender = newPatientInfo.genderEnum_enumText || newPatientInfo.gender || '';
|
||||||
|
formData.age = newPatientInfo.age || '';
|
||||||
|
formData.nation = newPatientInfo.nation || '';
|
||||||
|
formData.occupation = newPatientInfo.profession || '';
|
||||||
|
formData.marriage = newPatientInfo.maritalStatus || '';
|
||||||
|
formData.birthplace = newPatientInfo.birthPlace || '';
|
||||||
|
// 可以根据需要更新更多字段
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化记录时间为当前时间
|
// 初始化记录时间为当前时间
|
||||||
@@ -578,16 +598,16 @@ onMounted(() => {
|
|||||||
formData.signDate = formatDateTime(new Date());
|
formData.signDate = formatDateTime(new Date());
|
||||||
}
|
}
|
||||||
if (!formData.patientName) {
|
if (!formData.patientName) {
|
||||||
formData.patientName = patient?.patientName || '';
|
formData.patientName = patient?.patientName || patient?.name || '';
|
||||||
}
|
}
|
||||||
if (!formData.gender) {
|
if (!formData.gender) {
|
||||||
formData.gender = patient?.genderEnum_enumText || '';
|
formData.gender = patient?.genderEnum_enumText || patient?.gender || '';
|
||||||
}
|
}
|
||||||
if (!formData.age) {
|
if (!formData.age) {
|
||||||
formData.age = patient?.age || '';
|
formData.age = patient?.age || '';
|
||||||
}
|
}
|
||||||
if (!formData.hospitalNo) {
|
if (!formData.hospitalNo) {
|
||||||
formData.hospitalNo = patient?.busNo || '';
|
formData.hospitalNo = patient?.busNo || patient?.hospitalNo || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -334,11 +334,13 @@ export function handleColor(a, b, status) {
|
|||||||
*/
|
*/
|
||||||
export function getPrinterList() {
|
export function getPrinterList() {
|
||||||
try {
|
try {
|
||||||
const printerList =
|
if (window.hiprint && window.hiprint.hiwebSocket && window.hiprint.hiwebSocket.connected) {
|
||||||
window.hiprint && window.hiprint.hiwebSocket
|
const printerList = window.hiprint.hiwebSocket.getPrinterList();
|
||||||
? window.hiprint.hiwebSocket.getPrinterList()
|
|
||||||
: [];
|
|
||||||
return printerList || [];
|
return printerList || [];
|
||||||
|
} else {
|
||||||
|
console.warn('打印服务未连接,返回空打印机列表');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取打印机列表失败:', error);
|
console.error('获取打印机列表失败:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -151,11 +151,13 @@ export const PRINT_TEMPLATE = {
|
|||||||
*/
|
*/
|
||||||
export function getPrinterList() {
|
export function getPrinterList() {
|
||||||
try {
|
try {
|
||||||
const printerList =
|
if (window.hiprint && window.hiprint.hiwebSocket && window.hiprint.hiwebSocket.connected) {
|
||||||
window.hiprint && window.hiprint.hiwebSocket
|
const printerList = window.hiprint.hiwebSocket.getPrinterList();
|
||||||
? window.hiprint.hiwebSocket.getPrinterList()
|
|
||||||
: [];
|
|
||||||
return printerList || [];
|
return printerList || [];
|
||||||
|
} else {
|
||||||
|
console.warn('打印服务未连接,返回空打印机列表');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取打印机列表失败:', error);
|
console.error('获取打印机列表失败:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -225,7 +225,15 @@ function getList() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
getDiagnosisTreatmentList(queryParams.value).then((res) => {
|
getDiagnosisTreatmentList(queryParams.value).then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
catagoryList.value = res.data.records;
|
catagoryList.value = res.data.records.map(record => {
|
||||||
|
// 为每一行初始化 filteredOptions,确保显示框能正确显示项目名称
|
||||||
|
const filteredOptions = allImplementDepartmentList.value.slice(0, 100);
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
loading: false,
|
||||||
|
filteredOptions: filteredOptions
|
||||||
|
};
|
||||||
|
});
|
||||||
total.value = res.data.total;
|
total.value = res.data.total;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="text" @click="editTemplate(row)">编辑</el-button>
|
<el-button link @click="editTemplate(row)">编辑</el-button>
|
||||||
<el-button type="text" @click="deleteTemplate(row.id)" style="color: #f56c6c">
|
<el-button link @click="deleteTemplate(row.id)" style="color: #f56c6c">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -776,6 +776,7 @@ const transformFormData = (form) => {
|
|||||||
extraDetails,
|
extraDetails,
|
||||||
comment,
|
comment,
|
||||||
busNo,
|
busNo,
|
||||||
|
practitionerId: form.value.practitionerId,
|
||||||
},
|
},
|
||||||
chargeItemDefinitionFormData: {
|
chargeItemDefinitionFormData: {
|
||||||
id,
|
id,
|
||||||
@@ -833,6 +834,7 @@ const transformFormEditData = (form) => {
|
|||||||
extraDetails,
|
extraDetails,
|
||||||
comment,
|
comment,
|
||||||
busNo,
|
busNo,
|
||||||
|
practitionerId: form.value.practitionerId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {defineEmits, ref, unref} from 'vue';
|
|||||||
import {deleteRecord, getRecordByEncounterIdList} from '../api';
|
import {deleteRecord, getRecordByEncounterIdList} from '../api';
|
||||||
import {ElMessage} from 'element-plus';
|
import {ElMessage} from 'element-plus';
|
||||||
import {patientInfo} from '../../store/patient.js';
|
import {patientInfo} from '../../store/patient.js';
|
||||||
import apiRequestManager from '@/utils/apiRequestManager.js';
|
|
||||||
|
|
||||||
const emits = defineEmits(['historyClick']);
|
const emits = defineEmits(['historyClick']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -68,30 +67,15 @@ const queryParams = ref({
|
|||||||
isPage: 0,
|
isPage: 0,
|
||||||
});
|
});
|
||||||
const historyData = ref([]);
|
const historyData = ref([]);
|
||||||
// 防止重复加载的标志
|
|
||||||
let isLoadingHistory = false;
|
|
||||||
|
|
||||||
const queryList = async () => {
|
const queryList = async () => {
|
||||||
// 防止重复加载
|
|
||||||
if (isLoadingHistory) {
|
|
||||||
console.log('History data is already loading, skipping duplicate call');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingHistory = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
||||||
const res = await apiRequestManager.execute(
|
const res = await getRecordByEncounterIdList({
|
||||||
getRecordByEncounterIdList,
|
...queryParams.value,
|
||||||
'/document/record/getRecordByEncounterIdList',
|
|
||||||
{
|
|
||||||
isPage: 0, // 确保参数一致,便于去重
|
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: unref(definitionId),
|
definitionId: unref(definitionId),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
historyData.value = res.data || [];
|
historyData.value = res.data || [];
|
||||||
} else {
|
} else {
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
@@ -99,8 +83,6 @@ const queryList = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ElMessage.error(' 获取模板树失败 ');
|
// ElMessage.error(' 获取模板树失败 ');
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
} finally {
|
|
||||||
isLoadingHistory = false; // 重置加载标志
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleNodeClick = (data) => {
|
const handleNodeClick = (data) => {
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ import dayjs from 'dayjs';
|
|||||||
// 打印工具
|
// 打印工具
|
||||||
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
|
import {PRINT_TEMPLATE, simplePrint} from '@/utils/printUtils.js';
|
||||||
import {getEncounterDiagnosis} from '../api';
|
import {getEncounterDiagnosis} from '../api';
|
||||||
import apiRequestManager from '@/utils/apiRequestManager.js';
|
|
||||||
import History from './components/history';
|
import History from './components/history';
|
||||||
import Template from './components/template';
|
import Template from './components/template';
|
||||||
import TemplateEdit from './components/templateEdit.vue';
|
import TemplateEdit from './components/templateEdit.vue';
|
||||||
@@ -206,7 +205,7 @@ const handleNodeClick = (data, node) => {
|
|||||||
|
|
||||||
// 选择任何病历模板后,都加载该病历类型的最新历史记录
|
// 选择任何病历模板后,都加载该病历类型的最新历史记录
|
||||||
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
|
if (node.isLeaf && props.patientInfo && props.patientInfo.patientId) {
|
||||||
debouncedLoadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
@@ -280,7 +279,7 @@ const handleSubmitOk = async (data) => {
|
|||||||
|
|
||||||
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
debouncedLoadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('提交失败');
|
ElMessage.error('提交失败');
|
||||||
@@ -411,7 +410,7 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
// 等待模板加载完成,然后获取并回显最新病历数据
|
// 等待模板加载完成,然后获取并回显最新病历数据
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
historyRef.value?.queryList();
|
historyRef.value?.queryList();
|
||||||
debouncedLoadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -422,36 +421,19 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
// 当前选中的历史病历ID,用于在History组件中高亮显示
|
// 当前选中的历史病历ID,用于在History组件中高亮显示
|
||||||
const selectedHistoryRecordId = ref('');
|
const selectedHistoryRecordId = ref('');
|
||||||
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
|
|
||||||
// 防止重复加载的标志
|
|
||||||
let isLoadingLatestRecord = false;
|
|
||||||
|
|
||||||
// 加载最新的病历数据并回显
|
// 加载最新的病历数据并回显
|
||||||
const loadLatestMedicalRecord = async () => {
|
const loadLatestMedicalRecord = async () => {
|
||||||
if (!patientInfo.value.encounterId || !currentSelectTemplate.value.id) return;
|
if (!patientInfo.value.encounterId || !currentSelectTemplate.value.id) return;
|
||||||
|
|
||||||
// 防止重复加载
|
|
||||||
if (isLoadingLatestRecord) {
|
|
||||||
console.log('Latest medical record is already loading, skipping duplicate call');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingLatestRecord = true;
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取患者的历史病历记录
|
// 获取患者的历史病历记录
|
||||||
const res = await apiRequestManager.execute(
|
const res = await getRecordByEncounterIdList({
|
||||||
getRecordByEncounterIdList,
|
|
||||||
'/document/record/getRecordByEncounterIdList',
|
|
||||||
{
|
|
||||||
isPage: 0,
|
isPage: 0,
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: currentSelectTemplate.value.id,
|
definitionId: currentSelectTemplate.value.id,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const historyRecords = res.data || [];
|
const historyRecords = res.data || [];
|
||||||
if (historyRecords.length > 0) {
|
if (historyRecords.length > 0) {
|
||||||
@@ -537,12 +519,8 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
isLoadingLatestRecord = false; // 重置加载标志
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防抖版本的加载最新病历数据函数
|
|
||||||
const debouncedLoadLatestMedicalRecord = debounce(loadLatestMedicalRecord, 300);
|
|
||||||
const templateRef = ref(null);
|
const templateRef = ref(null);
|
||||||
|
|
||||||
const handleTemplateClick = (data) => {
|
const handleTemplateClick = (data) => {
|
||||||
@@ -772,7 +750,7 @@ const selectDefaultTemplate = () => {
|
|||||||
|
|
||||||
// 直接加载最新病历数据,不再使用额外的setTimeout延迟
|
// 直接加载最新病历数据,不再使用额外的setTimeout延迟
|
||||||
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
|
// 因为handleNodeClick中已经有nextTick和setTimeout处理组件渲染
|
||||||
debouncedLoadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('未找到门诊病历模板');
|
console.log('未找到门诊病历模板');
|
||||||
|
|||||||
@@ -59,8 +59,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="120" align="center">
|
<el-table-column label="操作" width="120" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="text" size="small" @click="handlePrint(scope.row)">打印</el-button>
|
<el-button link size="small" @click="handlePrint(scope.row)">打印</el-button>
|
||||||
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button link size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="selected-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
|
<div class="selected-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
|
||||||
<span style="font-weight: bold; color: #1a2b6d">已选择</span>
|
<span style="font-weight: bold; color: #1a2b6d">已选择</span>
|
||||||
<el-button type="text" @click="clearAllSelected" style="color: #f56c6c">清空</el-button>
|
<el-button link @click="clearAllSelected" style="color: #f56c6c">清空</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已选项目列表 -->
|
<!-- 已选项目列表 -->
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
<span class="item-itemName">{{ item.itemName }}</span>
|
<span class="item-itemName">{{ item.itemName }}</span>
|
||||||
<span class="item-price">¥{{ item.itemPrice }}</span>
|
<span class="item-price">¥{{ item.itemPrice }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
type="text"
|
link
|
||||||
size="small"
|
size="small"
|
||||||
style="color: #f56c6c; margin-left: auto"
|
style="color: #f56c6c; margin-left: auto"
|
||||||
@click="removeInspectionItem(item)"
|
@click="removeInspectionItem(item)"
|
||||||
|
|||||||
@@ -268,14 +268,15 @@ const validatePhraseName = (phraseName, excludeId = null) => {
|
|||||||
// 所有数据(用于客户端分页处理)
|
// 所有数据(用于客户端分页处理)
|
||||||
const allData = ref([])
|
const allData = ref([])
|
||||||
|
|
||||||
|
// 获取医生常用语列表数据
|
||||||
// 获取医生常用语列表数据
|
// 获取医生常用语列表数据
|
||||||
const fetchDoctorPhraseList = async () => {
|
const fetchDoctorPhraseList = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getDoctorPhraseList()
|
const response = await getDoctorPhraseList()
|
||||||
// 处理后端返回的数据结构:data.data
|
// 【关键修改】去掉 response.data.data,直接取 response.data
|
||||||
if (response.code === 200 && response.data && response.data.data) {
|
if (response.code === 200 && response.data) {
|
||||||
// 按照sortNo由小到大排序,保证列表顺序正确
|
// 按照sortNo由小到大排序,保证列表顺序正确
|
||||||
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
|
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
|
||||||
total.value = allData.value.length
|
total.value = allData.value.length
|
||||||
// 执行客户端分页逻辑
|
// 执行客户端分页逻辑
|
||||||
applyPagination()
|
applyPagination()
|
||||||
@@ -285,7 +286,7 @@ const fetchDoctorPhraseList = async () => {
|
|||||||
total.value = 0
|
total.value = 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取列表失败:', error) // 增加控制台日志便于调试
|
console.error('获取列表失败:', error)
|
||||||
ElMessage.error('获取数据失败: 网络请求错误')
|
ElMessage.error('获取数据失败: 网络请求错误')
|
||||||
allData.value = []
|
allData.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
@@ -322,19 +323,18 @@ const handleCurrentChange = (val) => {
|
|||||||
applyPagination()
|
applyPagination()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索功能核心方法
|
||||||
// 搜索功能核心方法
|
// 搜索功能核心方法
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
try {
|
try {
|
||||||
// searchScope可能是null(未选择)、1=个人,2=科室,3=全院
|
|
||||||
const phraseType = searchScope.value === null ? undefined : searchScope.value
|
const phraseType = searchScope.value === null ? undefined : searchScope.value
|
||||||
// 调用搜索接口:phraseName, phraseType
|
|
||||||
const response = await searchDoctorPhraseList(searchKeyword.value, phraseType)
|
const response = await searchDoctorPhraseList(searchKeyword.value, phraseType)
|
||||||
if (response.code === 200 && response.data && response.data.data) {
|
// 【关键修改】去掉 response.data.data,直接取 response.data
|
||||||
// 按照sortNo由小到大排序
|
if (response.code === 200 && response.data) {
|
||||||
allData.value = response.data.data.sort((a, b) => a.sortNo - b.sortNo)
|
allData.value = response.data.sort((a, b) => a.sortNo - b.sortNo)
|
||||||
total.value = allData.value.length
|
total.value = allData.value.length
|
||||||
currentPage.value = 1 // 搜索后重置到第一页
|
currentPage.value = 1
|
||||||
applyPagination() // 应用分页
|
applyPagination()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('搜索失败: ' + (response.msg || '未知错误'))
|
ElMessage.error('搜索失败: ' + (response.msg || '未知错误'))
|
||||||
allData.value = []
|
allData.value = []
|
||||||
@@ -349,20 +349,30 @@ const handleSearch = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 打开新增模态框方法
|
// 打开新增模态框方法
|
||||||
|
// index.vue
|
||||||
|
|
||||||
const showAddDialog = () => {
|
const showAddDialog = () => {
|
||||||
// 重置表单数据
|
// 1. 算出当前最大的排序号
|
||||||
|
// 如果列表是空的,就从 1 开始;如果不空,取第一条(因为我们排过序了)或遍历找最大值
|
||||||
|
let maxSortNo = 0
|
||||||
|
if (allData.value && allData.value.length > 0) {
|
||||||
|
// 既然 allData 已经按 sortNo 排序了,那最后一个就是最大的?
|
||||||
|
// 或者保险起见,用 Math.max 算一下
|
||||||
|
maxSortNo = Math.max(...allData.value.map(item => item.sortNo || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 重置表单,并将排序号设为 最大值 + 1
|
||||||
addForm.value = {
|
addForm.value = {
|
||||||
phraseName: '',
|
phraseName: '',
|
||||||
phraseContent: '',
|
phraseContent: '',
|
||||||
sortNo: 1,
|
sortNo: maxSortNo + 1, // <--- 这样每次打开就是 2, 3, 4...
|
||||||
phraseType: 1,
|
phraseType: 1,
|
||||||
phraseCategory: ''
|
phraseCategory: ''
|
||||||
}
|
}
|
||||||
// 重置表单验证状态
|
|
||||||
if (addFormRef.value) {
|
if (addFormRef.value) {
|
||||||
addFormRef.value.clearValidate()
|
addFormRef.value.clearValidate()
|
||||||
}
|
}
|
||||||
// 打开模态框
|
|
||||||
addDialogVisible.value = true
|
addDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +444,6 @@ const handleDelete = async (row) => {
|
|||||||
// 用户取消删除时不提示错误
|
// 用户取消删除时不提示错误
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除失败:', error)
|
console.error('删除失败:', error)
|
||||||
ElMessage.error('删除操作失败: 网络异常或权限不足')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,39 +464,41 @@ const showEditDialog = (row) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑表单提交保存方法
|
// 编辑表单提交保存方法
|
||||||
|
// 修改 index.vue 中的 handleEditSave 方法
|
||||||
const handleEditSave = async () => {
|
const handleEditSave = async () => {
|
||||||
try {
|
try {
|
||||||
// 先执行表单验证
|
// 1. 表单校验
|
||||||
const validateResult = await editFormRef.value.validate()
|
const validateResult = await editFormRef.value.validate()
|
||||||
if (!validateResult) return
|
if (!validateResult) return
|
||||||
|
|
||||||
// 名称唯一性校验(排除当前编辑的这条记录ID)
|
// 2. 名称唯一性校验
|
||||||
const nameValidation = validatePhraseName(editForm.value.phraseName, editForm.value.id)
|
const nameValidation = validatePhraseName(editForm.value.phraseName, editForm.value.id)
|
||||||
if (!nameValidation.valid) {
|
if (!nameValidation.valid) {
|
||||||
ElMessage.error(nameValidation.message)
|
ElMessage.error(nameValidation.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备更新数据,修复时间格式为ISO字符串,适配后端LocalDateTime
|
// 3. 准备数据
|
||||||
const updateData = {
|
const updateData = {
|
||||||
...editForm.value,
|
...editForm.value,
|
||||||
enableFlag: 1,
|
enableFlag: 1,
|
||||||
updateTime: new Date().toISOString() // 前端临时赋值,后端最终以自己的为准
|
updateTime: new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用更新接口
|
// 4. 调用接口
|
||||||
const response = await updateDoctorPhrase(updateData)
|
const response = await updateDoctorPhrase(updateData)
|
||||||
|
|
||||||
|
// 【核心修改】直接判断 code === 200 即可
|
||||||
|
// 因为后端现在失败会返回 R.fail (code!=200),所以只要是 200 就是成功
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success(response.msg || '更新成功') // 优先显示后端返回的消息
|
||||||
editDialogVisible.value = false
|
editDialogVisible.value = false
|
||||||
// 重新拉取数据,保证列表数据最新
|
|
||||||
fetchDoctorPhraseList()
|
fetchDoctorPhraseList()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('更新失败: ' + (response.msg || '未知错误'))
|
ElMessage.error(response.msg || '更新失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新失败:', error)
|
console.error('更新失败:', error)
|
||||||
ElMessage.error('更新操作失败: 网络请求错误')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,10 @@
|
|||||||
{{ userStore.nickName }}
|
{{ userStore.nickName }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="" width="300">
|
<el-descriptions-item label="" width="300">
|
||||||
|
<el-radio-group v-model="firstEnum">
|
||||||
|
<el-radio :label="1">初诊</el-radio>
|
||||||
|
<el-radio :label="2">复诊</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
<el-button type="primary" plain @click.stop="handleFinish(patientInfo.encounterId)">
|
<el-button type="primary" plain @click.stop="handleFinish(patientInfo.encounterId)">
|
||||||
完诊
|
完诊
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -209,7 +213,6 @@ import useUserStore from '@/store/modules/user';
|
|||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { updatePatientInfo } from './components/store/patient.js';
|
import { updatePatientInfo } from './components/store/patient.js';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
|
|
||||||
// // 监听路由离开事件
|
// // 监听路由离开事件
|
||||||
// onBeforeRouteLeave((to, from, next) => {
|
// onBeforeRouteLeave((to, from, next) => {
|
||||||
@@ -276,6 +279,7 @@ const loading = ref(false);
|
|||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const visitType = ref('');
|
const visitType = ref('');
|
||||||
const firstVisitDate = ref('');
|
const firstVisitDate = ref('');
|
||||||
|
const firstEnum = ref(1); // 初复诊标识:1=初诊,2=复诊
|
||||||
const disabled = computed(() => {
|
const disabled = computed(() => {
|
||||||
// 只有在有患者信息但某些条件不满足时才启用覆盖层
|
// 只有在有患者信息但某些条件不满足时才启用覆盖层
|
||||||
// 当前逻辑保持不变,但我们将在按钮级别处理禁用状态
|
// 当前逻辑保持不变,但我们将在按钮级别处理禁用状态
|
||||||
@@ -488,8 +492,7 @@ function handleOpen() {
|
|||||||
patientDrawerRef.value.refreshList();
|
patientDrawerRef.value.refreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原始的handleCardClick函数
|
function handleCardClick(item, index) {
|
||||||
function handleCardClickOriginal(item, index) {
|
|
||||||
console.log('handleCardClick 被调用');
|
console.log('handleCardClick 被调用');
|
||||||
console.log('点击的患者项目:', item);
|
console.log('点击的患者项目:', item);
|
||||||
console.log('患者项目中的encounterId:', item.encounterId);
|
console.log('患者项目中的encounterId:', item.encounterId);
|
||||||
@@ -506,6 +509,15 @@ function handleCardClickOriginal(item, index) {
|
|||||||
console.log('patientInfo.value 设置为:', patientInfo.value);
|
console.log('patientInfo.value 设置为:', patientInfo.value);
|
||||||
console.log('patientInfo.value.encounterId:', patientInfo.value?.encounterId);
|
console.log('patientInfo.value.encounterId:', patientInfo.value?.encounterId);
|
||||||
|
|
||||||
|
// 根据患者信息设置初复诊标识
|
||||||
|
const backendValue = item.firstEnum ?? item.first_enum;
|
||||||
|
|
||||||
|
if (backendValue !== undefined && backendValue !== null) {
|
||||||
|
firstEnum.value = Number(backendValue); // 确保是数字类型
|
||||||
|
} else {
|
||||||
|
firstEnum.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// 确保患者信息包含必要的字段
|
// 确保患者信息包含必要的字段
|
||||||
if (!patientInfo.value.encounterId) {
|
if (!patientInfo.value.encounterId) {
|
||||||
console.error('患者信息缺少encounterId字段:', patientInfo.value);
|
console.error('患者信息缺少encounterId字段:', patientInfo.value);
|
||||||
@@ -546,9 +558,6 @@ function handleCardClickOriginal(item, index) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用防抖的handleCardClick函数,防止短时间内多次点击
|
|
||||||
const handleCardClick = debounce(handleCardClickOriginal, 500);
|
|
||||||
|
|
||||||
function handleLeave(encounterId) {
|
function handleLeave(encounterId) {
|
||||||
leaveEncounter(encounterId).then((res) => {
|
leaveEncounter(encounterId).then((res) => {
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
@@ -566,11 +575,18 @@ function handleFinish(encounterId) {
|
|||||||
patientInfo.value = {};
|
patientInfo.value = {};
|
||||||
visitType.value = ''; // 重置初复诊标识
|
visitType.value = ''; // 重置初复诊标识
|
||||||
visitTypeDisabled.value = false; // 重置禁用状态
|
visitTypeDisabled.value = false; // 重置禁用状态
|
||||||
|
firstEnum.value = 1; // 重置为初诊
|
||||||
getPatientList();
|
getPatientList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听初复诊标识变化
|
||||||
|
watch(firstEnum, (newValue) => {
|
||||||
|
// 这里可以添加更新后端的逻辑,如果需要实时同步到后端
|
||||||
|
// 例如:updateEncounterFirstEnum(patientInfo.value.encounterId, newValue)
|
||||||
|
});
|
||||||
|
|
||||||
function handleTimeChange(value) {
|
function handleTimeChange(value) {
|
||||||
queryParams.value.registerTimeSTime = value + ' 00:00:00';
|
queryParams.value.registerTimeSTime = value + ' 00:00:00';
|
||||||
queryParams.value.registerTimeETime = value + ' 23:59:59';
|
queryParams.value.registerTimeETime = value + ' 23:59:59';
|
||||||
@@ -594,7 +610,7 @@ function handleHospitalizationClick() {
|
|||||||
|
|
||||||
// 接诊回调
|
// 接诊回调
|
||||||
function handleReceive(row) {
|
function handleReceive(row) {
|
||||||
handleCardClickOriginal(row);
|
handleCardClick(row);
|
||||||
currentEncounterId.value = row.encounterId;
|
currentEncounterId.value = row.encounterId;
|
||||||
drawer.value = false;
|
drawer.value = false;
|
||||||
getPatientList();
|
getPatientList();
|
||||||
@@ -781,7 +797,7 @@ const markSeen = async () => {
|
|||||||
currentCallPatient.value = {};
|
currentCallPatient.value = {};
|
||||||
};
|
};
|
||||||
const callThis = (row) => {
|
const callThis = (row) => {
|
||||||
handleCardClickOriginal(row);
|
handleCardClick(row);
|
||||||
currentCallPatient.value = row;
|
currentCallPatient.value = row;
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
// 刷新患者列表和候诊列表
|
// 刷新患者列表和候诊列表
|
||||||
|
|||||||
@@ -699,7 +699,12 @@ const getDom = () => {
|
|||||||
};
|
};
|
||||||
const setData = (data) => {
|
const setData = (data) => {
|
||||||
console.log('设置数据=========>', JSON.stringify(data));
|
console.log('设置数据=========>', JSON.stringify(data));
|
||||||
Object.assign(formData, data);
|
// 仅更新传入的数据,保留未传入字段的原有值
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (formData.hasOwnProperty(key)) {
|
||||||
|
formData[key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
defineExpose({
|
defineExpose({
|
||||||
setData,
|
setData,
|
||||||
|
|||||||
@@ -424,8 +424,8 @@ function handleWardClick(item) {
|
|||||||
|
|
||||||
function getInitOptions() {
|
function getInitOptions() {
|
||||||
getOrgList().then((res) => {
|
getOrgList().then((res) => {
|
||||||
// organization.value = res.data.records
|
// 直接从records中过滤,而不是从records[0].children
|
||||||
organization.value = res.data.records[0].children.filter(
|
organization.value = res.data.records.filter(
|
||||||
(record) => record.typeEnum === 2 && checkClassEnumValue(record.classEnum, 2)
|
(record) => record.typeEnum === 2 && checkClassEnumValue(record.classEnum, 2)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ watch(
|
|||||||
getList();
|
getList();
|
||||||
function getList() {
|
function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
queryParams.value.organizationId = props.patientInfo.inHospitalOrgId;
|
queryParams.value.organizationId = props.patientInfo?.inHospitalOrgId || '';
|
||||||
getAdviceBaseInfo(queryParams.value)
|
getAdviceBaseInfo(queryParams.value)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res.data.records);
|
console.log(res.data.records);
|
||||||
|
|||||||
@@ -203,7 +203,8 @@ const form = ref({
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
patientInfo: {
|
patientInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const emits = defineEmits(['diagnosisSave']);
|
const emits = defineEmits(['diagnosisSave']);
|
||||||
|
|||||||
@@ -10,25 +10,25 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="showApplicationFormDialog('LaboratoryTests')"
|
@click="showApplicationFormDialog('LaboratoryTests')"
|
||||||
:disabled="!patientInfo?.inHospitalOrgId"
|
:disabled="!props.patientInfo?.inHospitalOrgId"
|
||||||
>检验</el-button
|
>检验</el-button
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="showApplicationFormDialog('MedicalExaminations')"
|
@click="showApplicationFormDialog('MedicalExaminations')"
|
||||||
:disabled="!patientInfo?.inHospitalOrgId"
|
:disabled="!props.patientInfo?.inHospitalOrgId"
|
||||||
>检查</el-button
|
>检查</el-button
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="showApplicationFormDialog('BloodTransfusion')"
|
@click="showApplicationFormDialog('BloodTransfusion')"
|
||||||
:disabled="!patientInfo?.inHospitalOrgId"
|
:disabled="!props.patientInfo?.inHospitalOrgId"
|
||||||
>输血</el-button
|
>输血</el-button
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="showApplicationFormDialog('Surgery')"
|
@click="showApplicationFormDialog('Surgery')"
|
||||||
:disabled="!patientInfo?.inHospitalOrgId"
|
:disabled="!props.patientInfo?.inHospitalOrgId"
|
||||||
>手术</el-button
|
>手术</el-button
|
||||||
>
|
>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
@@ -58,14 +58,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, getCurrentInstance, nextTick, onBeforeMount, onMounted, reactive, ref,} from 'vue';
|
import {computed, getCurrentInstance, nextTick, onBeforeMount, onMounted, reactive, ref,} from 'vue';
|
||||||
import BloodTransfusion from './bloodTransfusion.vue';
|
import BloodTransfusion from './bloodTransfusion.vue';
|
||||||
import {patientInfo} from '../../../store/patient.js';
|
|
||||||
import Surgery from './surgery.vue';
|
import Surgery from './surgery.vue';
|
||||||
import LaboratoryTests from './laboratoryTests.vue';
|
import LaboratoryTests from './laboratoryTests.vue';
|
||||||
import MedicalExaminations from './medicalExaminations.vue';
|
import MedicalExaminations from './medicalExaminations.vue';
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const emits = defineEmits(['refResh']);
|
const emits = defineEmits(['refResh']);
|
||||||
const props = defineProps({});
|
const props = defineProps({
|
||||||
|
patientInfo: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
const state = reactive({});
|
const state = reactive({});
|
||||||
const components = ref({
|
const components = ref({
|
||||||
BloodTransfusion,
|
BloodTransfusion,
|
||||||
|
|||||||
@@ -297,16 +297,17 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<!-- // 底部按钮 -->
|
<!-- // 底部按钮 -->
|
||||||
<application-form-bottom-btn @refResh="refresh" />
|
<application-form-bottom-btn :patientInfo="patientInfo" @refResh="refresh" />
|
||||||
<OrderGroupDrawer
|
<OrderGroupDrawer
|
||||||
ref="orderFroupRef"
|
ref="orderFroupRef"
|
||||||
:diagnosis="diagnosisInfo"
|
:diagnosis="diagnosisInfo"
|
||||||
|
:organizationId="patientInfo?.orgId || ''"
|
||||||
@useOrderGroup="handleSaveGroup"
|
@useOrderGroup="handleSaveGroup"
|
||||||
/>
|
/>
|
||||||
<PrescriptionHistory
|
<PrescriptionHistory
|
||||||
ref="prescriptionHistoryRef"
|
ref="prescriptionHistoryRef"
|
||||||
:diagnosis="diagnosisInfo"
|
:diagnosis="diagnosisInfo"
|
||||||
:patientInfo="patientInfo"
|
:patientInfo="patientInfo || {}"
|
||||||
@userPrescriptionHistory="handleSaveHistory"
|
@userPrescriptionHistory="handleSaveHistory"
|
||||||
/>
|
/>
|
||||||
<LeaveHospitalDialog
|
<LeaveHospitalDialog
|
||||||
@@ -405,7 +406,8 @@ const buttonDisabled = computed(() => {
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
patientInfo: {
|
patientInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
activeTab: {
|
activeTab: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -525,33 +527,19 @@ function getList() {
|
|||||||
function refresh() {
|
function refresh() {
|
||||||
getListInfo(false);
|
getListInfo(false);
|
||||||
}
|
}
|
||||||
// 防止重复请求的标志
|
|
||||||
let listInfoRequestPromise = null;
|
|
||||||
|
|
||||||
// 获取列表信息
|
// 获取列表信息
|
||||||
function getListInfo(addNewRow) {
|
function getListInfo(addNewRow) {
|
||||||
// 如果已经有正在进行的请求,则返回该请求的Promise
|
|
||||||
if (listInfoRequestPromise) {
|
|
||||||
return listInfoRequestPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingInstance = ElLoading.service({ fullscreen: true });
|
loadingInstance = ElLoading.service({ fullscreen: true });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (loadingInstance) {
|
|
||||||
loadingInstance.close();
|
loadingInstance.close();
|
||||||
}
|
|
||||||
}, 180);
|
}, 180);
|
||||||
isAdding.value = false;
|
isAdding.value = false;
|
||||||
expandOrder.value = [];
|
expandOrder.value = [];
|
||||||
|
getPrescriptionList(patientInfo.value.encounterId).then((res) => {
|
||||||
|
console.log('getListInfo==========>', JSON.stringify(res.data));
|
||||||
|
|
||||||
// 并行请求两个API并将结果合并处理
|
loadingInstance.close();
|
||||||
listInfoRequestPromise = Promise.all([
|
prescriptionList.value = res.data
|
||||||
getPrescriptionList(patientInfo.value.encounterId),
|
|
||||||
getContract({ encounterId: patientInfo.value.encounterId })
|
|
||||||
])
|
|
||||||
.then(([prescriptionRes, contractRes]) => {
|
|
||||||
// 处理处方列表
|
|
||||||
prescriptionList.value = prescriptionRes.data
|
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
return {
|
return {
|
||||||
...JSON.parse(item.contentJson),
|
...JSON.parse(item.contentJson),
|
||||||
@@ -563,35 +551,15 @@ function getListInfo(addNewRow) {
|
|||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return new Date(b.requestTime) - new Date(a.requestTime);
|
return new Date(b.requestTime) - new Date(a.requestTime);
|
||||||
});
|
});
|
||||||
|
getGroupMarkers(); // 更新标记
|
||||||
// 处理合同列表
|
|
||||||
contractList.value = contractRes.data;
|
|
||||||
|
|
||||||
// 更新账户ID
|
|
||||||
accountId.value = patientInfo.value.accountId;
|
|
||||||
|
|
||||||
// 更新标记
|
|
||||||
getGroupMarkers();
|
|
||||||
|
|
||||||
if (props.activeTab == 'prescription' && addNewRow) {
|
if (props.activeTab == 'prescription' && addNewRow) {
|
||||||
handleAddPrescription();
|
handleAddPrescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('getListInfo==========>', JSON.stringify(prescriptionRes.data));
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取列表信息失败:', error);
|
|
||||||
ElMessage.error('获取列表信息失败');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (loadingInstance) {
|
|
||||||
loadingInstance.close();
|
|
||||||
}
|
|
||||||
// 请求完成后清除Promise引用
|
|
||||||
listInfoRequestPromise = null;
|
|
||||||
});
|
});
|
||||||
|
getContract({ encounterId: patientInfo.value.encounterId }).then((res) => {
|
||||||
return listInfoRequestPromise;
|
contractList.value = res.data;
|
||||||
|
});
|
||||||
|
accountId.value = patientInfo.value.accountId;
|
||||||
}
|
}
|
||||||
// 数据过滤
|
// 数据过滤
|
||||||
const filterPrescriptionList = computed(() => {
|
const filterPrescriptionList = computed(() => {
|
||||||
@@ -605,17 +573,8 @@ const filterPrescriptionList = computed(() => {
|
|||||||
return pList;
|
return pList;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 防止诊断信息重复请求的标志
|
|
||||||
let diagnosisInfoRequestPromise = null;
|
|
||||||
|
|
||||||
function getDiagnosisInfo() {
|
function getDiagnosisInfo() {
|
||||||
// 如果已经有正在进行的请求,则返回该请求的Promise
|
getEncounterDiagnosis(patientInfo.value.encounterId).then((res) => {
|
||||||
if (diagnosisInfoRequestPromise) {
|
|
||||||
return diagnosisInfoRequestPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnosisInfoRequestPromise = getEncounterDiagnosis(patientInfo.value.encounterId)
|
|
||||||
.then((res) => {
|
|
||||||
diagnosisList.value = res.data;
|
diagnosisList.value = res.data;
|
||||||
let diagnosisInfo = diagnosisList.value.filter((item) => {
|
let diagnosisInfo = diagnosisList.value.filter((item) => {
|
||||||
return item.maindiseFlag == 1;
|
return item.maindiseFlag == 1;
|
||||||
@@ -625,17 +584,7 @@ function getDiagnosisInfo() {
|
|||||||
conditionId.value = diagnosisInfo[0].conditionId;
|
conditionId.value = diagnosisInfo[0].conditionId;
|
||||||
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
|
encounterDiagnosisId.value = diagnosisInfo[0].encounterDiagnosisId;
|
||||||
diagnosisName.value = diagnosisInfo[0].name;
|
diagnosisName.value = diagnosisInfo[0].name;
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取诊断信息失败:', error);
|
|
||||||
ElMessage.error('获取诊断信息失败');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// 请求完成后清除Promise引用
|
|
||||||
diagnosisInfoRequestPromise = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return diagnosisInfoRequestPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowDisabled(row) {
|
function getRowDisabled(row) {
|
||||||
|
|||||||
@@ -65,40 +65,22 @@ const queryParams = ref({
|
|||||||
isPage: 0,
|
isPage: 0,
|
||||||
});
|
});
|
||||||
const historyData = ref([]);
|
const historyData = ref([]);
|
||||||
// 防止重复请求的标志
|
|
||||||
let queryListPromise = null;
|
|
||||||
|
|
||||||
const queryList = async () => {
|
const queryList = async () => {
|
||||||
// 如果已经有正在进行的请求,则返回该请求的Promise
|
|
||||||
if (queryListPromise) {
|
|
||||||
return queryListPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
if (patientInfo.value.encounterId && unref(definitionId) && unref(definitionId) !== '') {
|
||||||
queryListPromise = getRecordByEncounterIdList({
|
const res = await getRecordByEncounterIdList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
encounterId: patientInfo.value.encounterId,
|
encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
patientId: patientInfo.value.patientId,
|
||||||
definitionId: unref(definitionId),
|
definitionId: unref(definitionId),
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
historyData.value = res.data || [];
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// 请求完成后清除Promise引用
|
|
||||||
queryListPromise = null;
|
|
||||||
});
|
});
|
||||||
|
historyData.value = res.data || [];
|
||||||
return queryListPromise;
|
|
||||||
} else {
|
} else {
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 不显示错误消息,避免干扰用户体验
|
// 不显示错误消息,避免干扰用户体验
|
||||||
historyData.value = [];
|
historyData.value = [];
|
||||||
// 请求完成后清除Promise引用
|
|
||||||
queryListPromise = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,7 +103,7 @@ const handleDelete = async (item) => {
|
|||||||
|
|
||||||
await deleteRecord([item.id]);
|
await deleteRecord([item.id]);
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
queryList();
|
await queryList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('删除失败');
|
ElMessage.error('删除失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,11 +211,11 @@ const handleNodeClick = (data, node) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 先清空当前组件,再设置新组件,确保组件完全重新渲染
|
// 先清空当前组件,再设置新组件,确保组件完全重新渲染
|
||||||
currentComponent.value = '';
|
currentComponent.value = undefined;
|
||||||
|
|
||||||
// 使用 nextTick 确保 DOM 更新后再设置新组件
|
// 使用 nextTick 确保 DOM 更新后再设置新组件
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
currentComponent.value = currentSelectTemplate.value.vueRouter || '';
|
currentComponent.value = currentSelectTemplate.value.vueRouter;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
currentSelectTemplate.value = {
|
currentSelectTemplate.value = {
|
||||||
@@ -241,7 +241,7 @@ const handleNodeClick = (data, node) => {
|
|||||||
|
|
||||||
const newEmr = () => {
|
const newEmr = () => {
|
||||||
if (currentSelectTemplate.value) {
|
if (currentSelectTemplate.value) {
|
||||||
currentComponent.value = currentSelectTemplate.value.vueRouter || '';
|
currentComponent.value = currentSelectTemplate.value.vueRouter;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ElMessage.error('请选择模版!');
|
ElMessage.error('请选择模版!');
|
||||||
@@ -305,10 +305,9 @@ const handleSubmitOk = async (data) => {
|
|||||||
// templateRef.value?.queryList();
|
// templateRef.value?.queryList();
|
||||||
|
|
||||||
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
// 等待历史记录列表更新后,重新加载最新病历并更新选中状态
|
||||||
// 增加延迟时间以确保数据库更新完成
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadLatestMedicalRecord();
|
loadLatestMedicalRecord();
|
||||||
}, 300);
|
}, 100);
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('保存成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('提交失败');
|
ElMessage.error('提交失败');
|
||||||
@@ -504,7 +503,7 @@ const resetForm = async () => {
|
|||||||
// 先将组件设置为空,强制卸载
|
// 先将组件设置为空,强制卸载
|
||||||
const currentComponentName = currentComponent.value;
|
const currentComponentName = currentComponent.value;
|
||||||
if (currentComponentName) {
|
if (currentComponentName) {
|
||||||
currentComponent.value = '';
|
currentComponent.value = undefined;
|
||||||
|
|
||||||
// 等待DOM更新
|
// 等待DOM更新
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -554,30 +553,19 @@ const selectOutpatientMedicalRecordTemplate = async () => {
|
|||||||
selectDefaultTemplate();
|
selectDefaultTemplate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防止重复请求的标志
|
|
||||||
let loadLatestMedicalRecordPromise = null;
|
|
||||||
|
|
||||||
// 加载最新的病历数据并回显
|
// 加载最新的病历数据并回显
|
||||||
const loadLatestMedicalRecord = async () => {
|
const loadLatestMedicalRecord = async () => {
|
||||||
// 如果已经有正在进行的请求,则返回该请求的Promise
|
|
||||||
if (loadLatestMedicalRecordPromise) {
|
|
||||||
return loadLatestMedicalRecordPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
|
if (!patientInfo.value?.encounterId || !currentSelectTemplate.value.id) return;
|
||||||
editForm.value.id = '';
|
editForm.value.id = '';
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// 创建一个新的Promise来处理请求
|
|
||||||
loadLatestMedicalRecordPromise = new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
try {
|
||||||
// 获取患者的历史病历记录
|
// 获取患者的历史病历记录
|
||||||
const res = await getRecordByEncounterIdList({
|
// const res = await getRecordByEncounterIdList({
|
||||||
isPage: 0,
|
// isPage: 0,
|
||||||
encounterId: patientInfo.value.encounterId,
|
// encounterId: patientInfo.value.encounterId,
|
||||||
patientId: patientInfo.value.patientId,
|
// patientId: patientInfo.value.patientId,
|
||||||
definitionId: currentSelectTemplate.value.id,
|
// definitionId: currentSelectTemplate.value.id,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const historyRecords = res.data || [];
|
const historyRecords = res.data || [];
|
||||||
if (historyRecords.length > 0) {
|
if (historyRecords.length > 0) {
|
||||||
@@ -608,8 +596,6 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
|
if (historyRef.value && typeof historyRef.value.updateSelectedRecord === 'function') {
|
||||||
historyRef.value.updateSelectedRecord(latestRecord.id);
|
historyRef.value.updateSelectedRecord(latestRecord.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(); // 成功完成
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 清空选中状态
|
// 清空选中状态
|
||||||
@@ -633,7 +619,6 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (emrComponentRef.value) {
|
if (emrComponentRef.value) {
|
||||||
emrComponentRef.value.setFormData({});
|
emrComponentRef.value.setFormData({});
|
||||||
}
|
}
|
||||||
resolve(); // 成功完成
|
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -660,17 +645,11 @@ const loadLatestMedicalRecord = async () => {
|
|||||||
if (emrComponentRef.value) {
|
if (emrComponentRef.value) {
|
||||||
emrComponentRef.value.setFormData({});
|
emrComponentRef.value.setFormData({});
|
||||||
}
|
}
|
||||||
reject(error); // 错误完成
|
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
// 请求完成后清除Promise引用
|
|
||||||
loadLatestMedicalRecordPromise = null;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return loadLatestMedicalRecordPromise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择默认模板 - 获取住院病历分类下的第一个模板
|
// 选择默认模板 - 获取住院病历分类下的第一个模板
|
||||||
@@ -830,7 +809,7 @@ watch(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有患者信息,也要重置组件和表单数据
|
// 如果没有患者信息,也要重置组件和表单数据
|
||||||
currentComponent.value = '';
|
currentComponent.value = undefined;
|
||||||
editForm.value = {
|
editForm.value = {
|
||||||
id: '',
|
id: '',
|
||||||
definitionId: '',
|
definitionId: '',
|
||||||
@@ -856,41 +835,6 @@ watch(
|
|||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 合并两个监听器,避免重复触发
|
|
||||||
let patientChangeProcessing = false; // 防止重复处理
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [patientInfo.value?.encounterId, currentSelectTemplate.value?.id],
|
|
||||||
([newEncounterId, newTemplateId]) => {
|
|
||||||
// 当患者就诊ID或模板ID变化时,加载最新病历数据
|
|
||||||
if (newEncounterId && newTemplateId && !patientChangeProcessing) {
|
|
||||||
patientChangeProcessing = true;
|
|
||||||
|
|
||||||
// 添加延迟以确保模板数据已更新
|
|
||||||
nextTick(() => {
|
|
||||||
loadLatestMedicalRecord().finally(() => {
|
|
||||||
// 重置处理标志
|
|
||||||
patientChangeProcessing = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听模板选择变化,当模板选择变化时加载最新病历数据
|
|
||||||
watch(
|
|
||||||
() => currentSelectTemplate.value.id,
|
|
||||||
(newTemplateId) => {
|
|
||||||
// 当模板选择变化时,加载该模板的最新病历数据
|
|
||||||
if (newTemplateId) {
|
|
||||||
// 只要有模板ID,就尝试加载数据,不管之前是否有患者信息
|
|
||||||
// 因为可能是在切换患者后才选择模板
|
|
||||||
loadLatestMedicalRecord();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 移除日志
|
// 移除日志
|
||||||
await queryTemplateTree();
|
await queryTemplateTree();
|
||||||
@@ -914,7 +858,43 @@ const onPrint = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ state });
|
// 添加一个方法供父组件调用,处理患者切换
|
||||||
|
const handlePatientChange = (patient) => {
|
||||||
|
// 更新患者信息
|
||||||
|
// 注意:这里我们不直接修改patientInfo.value,因为它是由父组件通过watch管理的
|
||||||
|
// 我们只需要确保当前组件响应patientInfo的变化
|
||||||
|
|
||||||
|
// 重置当前组件,确保在切换患者时重新加载
|
||||||
|
// 但我们需要确保在组件重新加载后能正确设置患者信息
|
||||||
|
const selectedTemplateId = currentSelectTemplate.value?.id;
|
||||||
|
|
||||||
|
if (selectedTemplateId) {
|
||||||
|
// 先清空当前组件
|
||||||
|
currentComponent.value = undefined;
|
||||||
|
|
||||||
|
// 使用nextTick确保DOM更新后再重新加载组件和数据
|
||||||
|
nextTick(() => {
|
||||||
|
// 重新设置组件
|
||||||
|
const template = templateData.value?.find(t => t.document && t.document.id === selectedTemplateId);
|
||||||
|
if (template && template.document?.vueRouter) {
|
||||||
|
currentComponent.value = template.document.vueRouter;
|
||||||
|
|
||||||
|
// 再次使用nextTick确保组件已加载后再加载数据
|
||||||
|
nextTick(() => {
|
||||||
|
loadLatestMedicalRecord();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果找不到对应模板,尝试选择默认模板
|
||||||
|
selectDefaultTemplate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果没有选中模板,尝试选择默认模板
|
||||||
|
selectDefaultTemplate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ state, handlePatientChange });
|
||||||
|
|
||||||
const disNode = () => {
|
const disNode = () => {
|
||||||
leftShow.value = !leftShow.value;
|
leftShow.value = !leftShow.value;
|
||||||
|
|||||||
@@ -109,25 +109,16 @@ const getList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标记是否已经手动选择过患者,防止后续自动选择
|
|
||||||
const hasManuallySelectedPatient = ref(false);
|
|
||||||
|
|
||||||
// 添加一个变量来跟踪当前期望的患者ID
|
|
||||||
let expectedPatientId = null;
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filteredCardData.value,
|
() => filteredCardData.value,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
|
// 如果有数据且当前没有选中患者,且是首次加载,默认选择第一条
|
||||||
// 只有在从未手动选择过患者的情况下才自动选择
|
|
||||||
// 并且确保当前没有正在处理的患者切换操作
|
|
||||||
if (
|
if (
|
||||||
newData &&
|
newData &&
|
||||||
newData.length > 0 &&
|
newData.length > 0 &&
|
||||||
!cardId.value &&
|
!cardId.value &&
|
||||||
isFirstLoad.value &&
|
isFirstLoad.value &&
|
||||||
!patientInfo.value?.encounterId &&
|
!patientInfo.value?.encounterId
|
||||||
!hasManuallySelectedPatient.value
|
|
||||||
) {
|
) {
|
||||||
const firstPatient = newData[0];
|
const firstPatient = newData[0];
|
||||||
if (firstPatient?.encounterId) {
|
if (firstPatient?.encounterId) {
|
||||||
@@ -139,81 +130,34 @@ watch(
|
|||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
handleItemClick(firstPatient);
|
handleItemClick(firstPatient);
|
||||||
isFirstLoad.value = false;
|
isFirstLoad.value = false;
|
||||||
hasManuallySelectedPatient.value = true; // 标记已手动选择过
|
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
} else if (expectedPatientId && cardId.value && cardId.value !== expectedPatientId) {
|
|
||||||
// 如果当前cardId与期望的不一致,且不是初始状态,这可能意味着发生了意外的重置
|
|
||||||
// 这种情况下,我们不希望自动选择第一个患者
|
|
||||||
console.debug(`期望的患者ID: ${expectedPatientId}, 当前cardId: ${cardId.value}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新handleItemClick函数,设置期望的患者ID
|
// 防抖函数,防止快速点击导致状态冲突
|
||||||
|
let debounceTimer = null;
|
||||||
const handleItemClick = (node) => {
|
const handleItemClick = (node) => {
|
||||||
// 设置期望的患者ID
|
|
||||||
expectedPatientId = node.encounterId;
|
|
||||||
|
|
||||||
// 清除之前的计时器
|
// 清除之前的计时器
|
||||||
if (debounceTimer) {
|
if (debounceTimer) {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消之前未完成的患者加载操作
|
|
||||||
if (currentPatientPromise) {
|
|
||||||
// 注意:这里无法真正取消Promise,但我们可以标记当前操作已过期
|
|
||||||
currentPatientPromise.cancelled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的计时器
|
// 设置新的计时器
|
||||||
debounceTimer = setTimeout(async () => {
|
debounceTimer = setTimeout(() => {
|
||||||
// 检查是否已被取消
|
|
||||||
if (currentPatientPromise?.cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardId.value = node.encounterId;
|
cardId.value = node.encounterId;
|
||||||
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
|
// 同时更新本地和全局状态,确保模块内组件和跨模块组件都能正确响应
|
||||||
updatePatientInfo(node);
|
updatePatientInfo(node);
|
||||||
updateLocalPatientInfo(node);
|
updateLocalPatientInfo(node);
|
||||||
|
|
||||||
// 标记已手动选择患者,防止自动选择第一条
|
diagnosisRef.value?.getList();
|
||||||
hasManuallySelectedPatient.value = true;
|
adviceRef.value?.getListInfo();
|
||||||
|
adviceRef.value?.getDiagnosisInfo();
|
||||||
// 创建一个新的Promise来追踪这次加载操作
|
|
||||||
currentPatientPromise = Promise.all([
|
|
||||||
// 并行调用医嘱相关的API,避免重复请求
|
|
||||||
adviceRef.value?.getListInfo().catch(error => {
|
|
||||||
console.error('获取医嘱信息失败:', error);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
adviceRef.value?.getDiagnosisInfo().catch(error => {
|
|
||||||
console.error('获取诊断信息失败:', error);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
// 获取诊断信息
|
|
||||||
diagnosisRef.value?.getList?.().catch(error => {
|
|
||||||
console.error('获取诊断信息失败:', error);
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await currentPatientPromise;
|
|
||||||
// 检查在此期间是否选择了其他患者
|
|
||||||
if (currentPatientPromise?.cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载患者信息时出错:', error);
|
|
||||||
}
|
|
||||||
}, 100); // 100ms 防抖延迟
|
}, 100); // 100ms 防抖延迟
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防抖函数,防止快速点击导致状态冲突
|
|
||||||
|
|
||||||
const handleSearch = (keyword) => {
|
const handleSearch = (keyword) => {
|
||||||
searchData.keyword = keyword;
|
searchData.keyword = keyword;
|
||||||
getList();
|
getList();
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" align="center">
|
<el-table-column label="操作" width="100" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="text" size="small" @click="feeItemsList.splice(scope.$index, 1)">
|
<el-button link size="small" @click="feeItemsList.splice(scope.$index, 1)">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@
|
|||||||
<el-table-column label="金额" prop="amount" width="100" align="center" />
|
<el-table-column label="金额" prop="amount" width="100" align="center" />
|
||||||
<el-table-column label="退费审核" width="80" align="center">
|
<el-table-column label="退费审核" width="80" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="text" style="color: #409eff">退费申请</el-button>
|
<el-button link style="color: #409eff">退费申请</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div>
|
<!-- <div>
|
||||||
<el-button size="small" type="text" @click="exportData">
|
<el-button size="small" link @click="exportData">
|
||||||
<el-icon><Download /></el-icon> 导出数据
|
<el-icon><Download /></el-icon> 导出数据
|
||||||
</el-button>
|
</el-button>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="call-number-display">
|
<div class="call-number-display" ref="screenContainer">
|
||||||
<!-- 头部区域 -->
|
<!-- 头部区域 -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{ departmentName }}</h1>
|
<h1>{{ departmentName }}</h1>
|
||||||
|
<div class="header-right">
|
||||||
|
<button class="fullscreen-btn" @click="toggleFullscreen">
|
||||||
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||||
|
</button>
|
||||||
<div class="time">{{ currentTime }}</div>
|
<div class="time">{{ currentTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 当前呼叫区 -->
|
<!-- 当前呼叫区 -->
|
||||||
<div class="current-call">
|
<div class="current-call">
|
||||||
@@ -31,19 +36,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template v-for="doctorName in paginatedDoctors" :key="doctorName">
|
<template v-for="doctorName in paginatedDoctors">
|
||||||
<template v-if="groupedPatients[doctorName]">
|
<template v-if="groupedPatients[doctorName]">
|
||||||
<!-- 医生分组标题 -->
|
<!-- 医生分组标题 -->
|
||||||
<tr class="doctor-header">
|
<tr class="doctor-header" :key="`doctor-${doctorName}`">
|
||||||
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- 患者列表 -->
|
<!-- 患者列表 -->
|
||||||
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="patient.id">
|
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="`${doctorName}-${patient.id}`">
|
||||||
<td>{{ index + 1 }}</td>
|
<td>{{ index + 1 }}</td>
|
||||||
<td>{{ formatPatientName(patient.name) }}</td>
|
<td>{{ patient.name }}</td>
|
||||||
<td>{{ getDoctorRoom(doctorName) }}</td>
|
<td>{{ getDoctorRoom(doctorName) }}</td>
|
||||||
<td :style="{ color: index === 0 ? '#e74c3c' : '#27ae60' }">
|
<td :style="{ color: patient.status === 'CALLING' ? '#e74c3c' : '#27ae60' }">
|
||||||
{{ index === 0 ? '就诊中' : '等待' }}
|
{{ patient.status === 'CALLING' ? '就诊中' : '等待' }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,49 +93,67 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
|
||||||
|
// ========== 配置参数 ==========
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { orgId: userOrgId, tenantId: userTenantId } = storeToRefs(userStore)
|
||||||
|
|
||||||
|
// 从登录用户获取科室ID(避免硬编码;后端已确保 orgId 以字符串返回)
|
||||||
|
const ORGANIZATION_ID = computed(() => (userOrgId.value ? String(userOrgId.value) : ''))
|
||||||
|
const TENANT_ID = computed(() => (userTenantId.value ? Number(userTenantId.value) : 1))
|
||||||
|
const API_BASE_URL = '/triage/queue'
|
||||||
|
// SSE 地址(走后端 API 代理)
|
||||||
|
const SSE_URL = computed(() => {
|
||||||
|
const baseApi = import.meta.env.VITE_APP_BASE_API || ''
|
||||||
|
const orgId = ORGANIZATION_ID.value
|
||||||
|
const tenantId = TENANT_ID.value
|
||||||
|
return `${baseApi}${API_BASE_URL}/display/stream?organizationId=${encodeURIComponent(orgId)}&tenantId=${tenantId}`
|
||||||
|
})
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
const currentCall = ref({
|
const currentCall = ref(null)
|
||||||
number: '1',
|
|
||||||
name: '李*四',
|
|
||||||
room: '3号'
|
|
||||||
})
|
|
||||||
const patients = ref([])
|
const patients = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const patientsPerPage = 5
|
const patientsPerPage = 5
|
||||||
const autoScrollInterval = ref(null)
|
const autoScrollInterval = ref(null)
|
||||||
const scrollInterval = 5000 // 5秒自动翻页
|
const scrollInterval = 5000 // 5秒自动翻页
|
||||||
|
const sseConnection = ref(null) // SSE 连接
|
||||||
|
const timeInterval = ref(null)
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
const screenContainer = ref(null)
|
||||||
|
let tableContainer = null
|
||||||
|
|
||||||
// 科室名称
|
// 科室名称
|
||||||
const departmentName = ref('心内科叫号显示屏幕')
|
const departmentName = ref('叫号显示屏幕')
|
||||||
|
|
||||||
// 计算属性
|
const applyDefaultDepartmentName = () => {
|
||||||
|
if (userStore.orgName) {
|
||||||
|
departmentName.value = `${userStore.orgName} 叫号显示屏`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待总人数(从后端返回)
|
||||||
|
const waitingCount = ref(0)
|
||||||
|
|
||||||
|
// 计算属性:按医生分组的患者列表
|
||||||
const groupedPatients = computed(() => {
|
const groupedPatients = computed(() => {
|
||||||
const grouped = {}
|
const grouped = {}
|
||||||
patients.value.forEach(patient => {
|
patients.value.forEach(doctorGroup => {
|
||||||
if (!grouped[patient.doctor]) {
|
grouped[doctorGroup.doctorName] = doctorGroup.patients || []
|
||||||
grouped[patient.doctor] = []
|
|
||||||
}
|
|
||||||
grouped[patient.doctor].push(patient)
|
|
||||||
})
|
})
|
||||||
return grouped
|
return grouped
|
||||||
})
|
})
|
||||||
|
|
||||||
const waitingCount = computed(() => {
|
|
||||||
let count = 0
|
|
||||||
Object.values(groupedPatients.value).forEach(group => {
|
|
||||||
count += Math.max(0, group.length - 1) // 排除每个医生组中第一个就诊中的患者
|
|
||||||
})
|
|
||||||
return count
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取排序后的医生列表
|
// 获取排序后的医生列表
|
||||||
const sortedDoctors = computed(() => {
|
const sortedDoctors = computed(() => {
|
||||||
return Object.keys(groupedPatients.value).sort()
|
return patients.value.map(group => group.doctorName)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 按医生分组的分页逻辑
|
// 按医生分组的分页逻辑
|
||||||
@@ -159,6 +182,24 @@ const updateTime = () => {
|
|||||||
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFullscreenState = () => {
|
||||||
|
const isActive = !!document.fullscreenElement
|
||||||
|
isFullscreen.value = isActive
|
||||||
|
document.body.classList.toggle('call-screen-fullscreen', isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = async () => {
|
||||||
|
try {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
await document.exitFullscreen()
|
||||||
|
} else if (screenContainer.value && screenContainer.value.requestFullscreen) {
|
||||||
|
await screenContainer.value.requestFullscreen()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换全屏失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatPatientName = (name) => {
|
const formatPatientName = (name) => {
|
||||||
if (!name || typeof name !== 'string') return '-'
|
if (!name || typeof name !== 'string') return '-'
|
||||||
if (name.length === 0) return '-'
|
if (name.length === 0) return '-'
|
||||||
@@ -166,49 +207,98 @@ const formatPatientName = (name) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDoctorRoom = (doctorName) => {
|
const getDoctorRoom = (doctorName) => {
|
||||||
// 根据医生获取固定诊室
|
// 从后端数据中查找医生的诊室号
|
||||||
const doctorRooms = {
|
const doctorGroup = patients.value.find(group => group.doctorName === doctorName)
|
||||||
'张医生': '3号',
|
return doctorGroup?.roomNo || '1号'
|
||||||
'李医生': '1号',
|
|
||||||
'王医生': '2号'
|
|
||||||
}
|
|
||||||
return doctorRooms[doctorName] || '1号'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateWaitingData = async () => {
|
const ensureUserInfo = async () => {
|
||||||
|
if (!userStore.orgId) {
|
||||||
try {
|
try {
|
||||||
|
await userStore.getInfo()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyDefaultDepartmentName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示屏数据(从后端API)
|
||||||
|
*/
|
||||||
|
const fetchDisplayData = async () => {
|
||||||
|
try {
|
||||||
|
if (!ORGANIZATION_ID.value) {
|
||||||
|
ElMessage.warning('未获取到登录用户科室信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// 确保数组已正确初始化
|
console.log('正在获取显示屏数据...', {
|
||||||
if (!Array.isArray(patients.value)) {
|
url: `${API_BASE_URL}/display`,
|
||||||
patients.value = []
|
organizationId: ORGANIZATION_ID.value,
|
||||||
|
tenantId: TENANT_ID.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
url: `${API_BASE_URL}/display`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
organizationId: ORGANIZATION_ID.value,
|
||||||
|
tenantId: TENANT_ID.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('后端响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
// 更新科室名称
|
||||||
|
if (data.departmentName && data.departmentName !== '叫号显示屏') {
|
||||||
|
departmentName.value = data.departmentName
|
||||||
|
} else {
|
||||||
|
applyDefaultDepartmentName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟API调用获取候诊数据
|
// 更新当前叫号信息
|
||||||
// 实际项目中这里应该调用真实API
|
if (data.currentCall) {
|
||||||
const mockData = [
|
currentCall.value = data.currentCall
|
||||||
{ id: 13, name: '李四', type: '专家', doctor: '张医生', status: '就诊中' },
|
} else {
|
||||||
{ id: 14, name: '王五', type: '普通', doctor: '李医生', status: '候诊中' },
|
currentCall.value = {
|
||||||
{ id: 15, name: '赵六', type: '专家', doctor: '张医生', status: '候诊中' },
|
number: null,
|
||||||
{ id: 16, name: '钱七', type: '普通', doctor: '王医生', status: '候诊中' },
|
name: '-',
|
||||||
{ id: 17, name: '孙八', type: '专家', doctor: '李医生', status: '候诊中' },
|
room: '-',
|
||||||
{ id: 18, name: '周九', type: '普通', doctor: '王医生', status: '候诊中' },
|
doctor: '-'
|
||||||
{ id: 19, name: '吴十', type: '专家', doctor: '张医生', status: '候诊中' },
|
}
|
||||||
{ id: 20, name: '郑一', type: '普通', doctor: '李医生', status: '候诊中' },
|
}
|
||||||
{ id: 21, name: '王二', type: '专家', doctor: '王医生', status: '候诊中' },
|
|
||||||
{ id: 22, name: '李三', type: '普通', doctor: '张医生', status: '候诊中' },
|
|
||||||
{ id: 23, name: '赵四', type: '专家', doctor: '李医生', status: '候诊中' },
|
|
||||||
{ id: 24, name: '钱五', type: '普通', doctor: '王医生', status: '候诊中' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟网络延迟
|
// 更新等候队列(按医生分组)
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||||
patients.value = mockData
|
patients.value = data.waitingList
|
||||||
} catch (error) {
|
console.log('等候队列数据:', data.waitingList)
|
||||||
console.error('获取候诊数据失败:', error)
|
} else {
|
||||||
ElMessage.error('获取候诊数据失败')
|
|
||||||
// 出错时设置为空数组
|
|
||||||
patients.value = []
|
patients.value = []
|
||||||
|
console.log('等候队列为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等待人数
|
||||||
|
waitingCount.value = data.waitingCount || 0
|
||||||
|
|
||||||
|
console.log('显示屏数据更新成功', data)
|
||||||
|
ElMessage.success('数据加载成功')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.msg || '获取数据失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取显示屏数据失败:', error)
|
||||||
|
ElMessage.error('获取显示屏数据失败:' + (error.message || '未知错误'))
|
||||||
|
|
||||||
|
// 出错时设置默认值
|
||||||
|
patients.value = []
|
||||||
|
currentCall.value = { number: null, name: '-', room: '-', doctor: '-' }
|
||||||
|
waitingCount.value = 0
|
||||||
|
applyDefaultDepartmentName()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -259,40 +349,158 @@ const stopAutoScroll = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 SSE 连接
|
||||||
|
*/
|
||||||
|
const initSse = () => {
|
||||||
|
try {
|
||||||
|
if (!ORGANIZATION_ID.value) {
|
||||||
|
console.warn('未获取到科室ID,跳过 SSE 连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sseConnection.value) {
|
||||||
|
sseConnection.value.close()
|
||||||
|
}
|
||||||
|
console.log('正在连接 SSE:', SSE_URL.value)
|
||||||
|
sseConnection.value = new EventSource(SSE_URL.value)
|
||||||
|
|
||||||
|
sseConnection.value.onopen = () => {
|
||||||
|
console.log('SSE 连接成功')
|
||||||
|
ElMessage.success('实时连接已建立')
|
||||||
|
}
|
||||||
|
|
||||||
|
sseConnection.value.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
console.log('收到 SSE 消息:', message)
|
||||||
|
|
||||||
|
if (message.type === 'init') {
|
||||||
|
handleSseUpdate(message.data)
|
||||||
|
} else if (message.type === 'update') {
|
||||||
|
handleSseUpdate(message.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析 SSE 消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sseConnection.value.onerror = (error) => {
|
||||||
|
console.error('SSE 错误:', error)
|
||||||
|
ElMessage.error('实时连接出现错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化 SSE 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SSE 推送的更新数据
|
||||||
|
*/
|
||||||
|
const handleSseUpdate = (data) => {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
// 更新科室名称
|
||||||
|
if (data.departmentName) {
|
||||||
|
departmentName.value = data.departmentName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前叫号信息
|
||||||
|
if (data.currentCall) {
|
||||||
|
currentCall.value = data.currentCall
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等候队列
|
||||||
|
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||||
|
patients.value = data.waitingList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等待人数
|
||||||
|
if (data.waitingCount !== undefined) {
|
||||||
|
waitingCount.value = data.waitingCount
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('显示屏数据已更新(来自 SSE)')
|
||||||
|
|
||||||
|
// 播放语音(如果有新的叫号)
|
||||||
|
if (data.currentCall && data.currentCall.number) {
|
||||||
|
playVoiceNotification(data.currentCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放语音通知
|
||||||
|
*/
|
||||||
|
const playVoiceNotification = (callInfo) => {
|
||||||
|
if (!callInfo || !callInfo.number) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 Web Speech API 播放语音
|
||||||
|
const utterance = new SpeechSynthesisUtterance(
|
||||||
|
`请${callInfo.number}号${callInfo.name}到${callInfo.room}诊室就诊`
|
||||||
|
)
|
||||||
|
utterance.lang = 'zh-CN'
|
||||||
|
utterance.rate = 0.9 // 语速
|
||||||
|
utterance.pitch = 1.0 // 音调
|
||||||
|
utterance.volume = 1.0 // 音量
|
||||||
|
|
||||||
|
window.speechSynthesis.speak(utterance)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('语音播放失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 SSE 连接
|
||||||
|
*/
|
||||||
|
const closeSse = () => {
|
||||||
|
if (sseConnection.value) {
|
||||||
|
sseConnection.value.close()
|
||||||
|
sseConnection.value = null
|
||||||
|
console.log('SSE 连接已关闭')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
document.addEventListener('fullscreenchange', updateFullscreenState)
|
||||||
|
await ensureUserInfo()
|
||||||
// 初始化时间
|
// 初始化时间
|
||||||
updateTime()
|
updateTime()
|
||||||
// 每分钟更新时间
|
// 每分钟更新时间
|
||||||
const timeInterval = setInterval(updateTime, 60000)
|
timeInterval.value = setInterval(updateTime, 60000)
|
||||||
|
|
||||||
// 获取候诊数据
|
// ✅ 获取初始数据(从后端 API)
|
||||||
await generateWaitingData()
|
await fetchDisplayData()
|
||||||
|
|
||||||
|
// ✅ 初始化 SSE 连接(实时推送)
|
||||||
|
initSse()
|
||||||
|
|
||||||
// 启动自动滚动
|
// 启动自动滚动
|
||||||
startAutoScroll()
|
startAutoScroll()
|
||||||
|
|
||||||
// 鼠标悬停时暂停自动滚动
|
// 鼠标悬停时暂停自动滚动
|
||||||
const tableContainer = document.querySelector('.table-container')
|
tableContainer = document.querySelector('.table-container')
|
||||||
if (tableContainer) {
|
if (tableContainer) {
|
||||||
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
||||||
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(timeInterval)
|
|
||||||
stopAutoScroll()
|
|
||||||
if (tableContainer) {
|
|
||||||
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
|
||||||
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时的清理工作
|
// 组件卸载时的清理工作
|
||||||
|
if (timeInterval.value) {
|
||||||
|
clearInterval(timeInterval.value)
|
||||||
|
timeInterval.value = null
|
||||||
|
}
|
||||||
stopAutoScroll()
|
stopAutoScroll()
|
||||||
|
closeSse() // ✅ 关闭 SSE 连接
|
||||||
|
if (tableContainer) {
|
||||||
|
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
||||||
|
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
||||||
|
tableContainer = null
|
||||||
|
}
|
||||||
|
document.removeEventListener('fullscreenchange', updateFullscreenState)
|
||||||
|
document.body.classList.remove('call-screen-fullscreen')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听页面变化,重置滚动位置
|
// 监听页面变化,重置滚动位置
|
||||||
@@ -317,6 +525,43 @@ watchEffect(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen) {
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .sidebar-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .navbar),
|
||||||
|
:global(body.call-screen-fullscreen .tags-view-container),
|
||||||
|
:global(body.call-screen-fullscreen #tags-view-container),
|
||||||
|
:global(body.call-screen-fullscreen .drawer-bg) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .app-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .main-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .content-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .app-main) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .app-wrapper) {
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .call-number-display) {
|
||||||
|
max-width: none;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 头部样式 */
|
/* 头部样式 */
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
||||||
@@ -342,6 +587,28 @@ watchEffect(() => {
|
|||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
border-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当前呼叫区 */
|
/* 当前呼叫区 */
|
||||||
|
|||||||
@@ -815,6 +815,8 @@ const loadDataFromApi = async () => {
|
|||||||
appointmentType: item.healthcareName ?? '普通',
|
appointmentType: item.healthcareName ?? '普通',
|
||||||
room: item.organizationName ? `${item.organizationName}` : '-',
|
room: item.organizationName ? `${item.organizationName}` : '-',
|
||||||
doctor: item.practitionerName ?? '-',
|
doctor: item.practitionerName ?? '-',
|
||||||
|
// 当前接口返回的是 practitionerUserId,保存为 practitionerId 供入队使用
|
||||||
|
practitionerId: item.practitionerUserId ?? null,
|
||||||
matchingRule: '-' // 这里先不做智能规则匹配
|
matchingRule: '-' // 这里先不做智能规则匹配
|
||||||
}))
|
}))
|
||||||
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
|
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
|
||||||
@@ -1031,7 +1033,9 @@ const handleAddToQueue = async () => {
|
|||||||
patientId: c.patientId,
|
patientId: c.patientId,
|
||||||
patientName: c.patientName,
|
patientName: c.patientName,
|
||||||
healthcareName: c.appointmentType,
|
healthcareName: c.appointmentType,
|
||||||
practitionerName: c.doctor
|
practitionerName: c.doctor,
|
||||||
|
practitionerId: c.practitionerId ?? null,
|
||||||
|
roomNo: c.roomNo ?? c.room ?? null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1144,7 +1148,9 @@ const handleAddAllToQueue = async () => {
|
|||||||
patientId: c.patientId,
|
patientId: c.patientId,
|
||||||
patientName: c.patientName,
|
patientName: c.patientName,
|
||||||
healthcareName: c.appointmentType,
|
healthcareName: c.appointmentType,
|
||||||
practitionerName: c.doctor
|
practitionerName: c.doctor,
|
||||||
|
practitionerId: c.practitionerId ?? null,
|
||||||
|
roomNo: c.roomNo ?? c.room ?? null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user