Compare commits
46 Commits
refactor/j
...
02874b59ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 02874b59ce | |||
| 092804557b | |||
|
|
48189a075f | ||
| a5b2faea3a | |||
|
|
c26b458298 | ||
| e38c5993a0 | |||
| ae746cdd37 | |||
|
|
cb82f8d5bf | ||
|
|
ea8dca058a | ||
| deb5683ca6 | |||
| c4ca097bf6 | |||
| b2c60ab76f | |||
|
|
8b6265801d | ||
| fec6e928d8 | |||
|
|
bf5a9674df | ||
| 471bf2b823 | |||
| 954462272e | |||
| 7b42e94b85 | |||
| 9a5d772c72 | |||
| d861c20d5e | |||
| 488573a51b | |||
| e35bdb5b9e | |||
|
|
259a5946c2 | ||
| d0d6cf3533 | |||
| 9ed35448ce | |||
|
|
fef1ca6637 | ||
|
|
fca3d0ca86 | ||
| 193a08acbd | |||
|
|
41d05a1629 | ||
|
|
8cfa6fe05e | ||
|
|
8eb6feb70d | ||
|
|
f93bec967a | ||
|
|
020d1be4be | ||
|
|
f7f037aee9 | ||
| 6c77ee8f84 | |||
| 0855d1153b | |||
|
|
168961e656 | ||
|
|
9dc4a12339 | ||
|
|
9bbf7c6c08 | ||
| 05088a1d1a | |||
|
|
5e9dbb2f1b | ||
|
|
b25d2fbaa9 | ||
| 690e7ca22c | |||
| 43ab5b4498 | |||
| 219ac30dc5 | |||
| 20f71ec5d9 |
@@ -1,7 +1,7 @@
|
||||
# HealthLink-HIS 代码模块索引
|
||||
|
||||
> 供 LLM 快速定位代码。每个模块列出 Controller → Service → Mapper 关键文件。
|
||||
> 最后更新: 2026-06-15 12:00 (300 个 Controller)
|
||||
> 最后更新: 2026-06-16 18:00 (300 个 Controller)
|
||||
|
||||
## 关键词 → 模块速查
|
||||
|
||||
|
||||
@@ -26,6 +26,18 @@ public class SysMenuController extends BaseController {
|
||||
@Autowired
|
||||
private ISysMenuService menuService;
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的菜单树(无需管理员权限)
|
||||
* 用于功能配置页面,让普通用户也能选择自己有权限的菜单
|
||||
*/
|
||||
@GetMapping("/userMenus")
|
||||
public AjaxResult userMenus() {
|
||||
Long userId = getUserId();
|
||||
List<SysMenu> menus = menuService.selectMenuList(new SysMenu(), userId);
|
||||
List<SysMenu> menuTreeWithFullPath = menuService.buildMenuTreeWithFullPath(menus);
|
||||
return success(menuTreeWithFullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
*/
|
||||
@@ -42,7 +54,7 @@ public class SysMenuController extends BaseController {
|
||||
* 根据菜单编号获取详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('system:menu:query')")
|
||||
@GetMapping(value = "/{menuId}")
|
||||
@GetMapping(value = "/{menuId:\\d+}")
|
||||
public AjaxResult getInfo(@PathVariable Long menuId) {
|
||||
return success(menuService.selectMenuById(menuId));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.core.framework.config;
|
||||
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -14,6 +15,9 @@ import tools.jackson.databind.ValueDeserializer;
|
||||
import tools.jackson.databind.ValueSerializer;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.TimeZone;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@@ -55,9 +59,18 @@ public class ApplicationConfig {
|
||||
@Bean
|
||||
public JsonMapperBuilderCustomizer jacksonObjectMapperCustomization() {
|
||||
return builder -> {
|
||||
builder.defaultTimeZone(TimeZone.getDefault());
|
||||
builder.defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
|
||||
SimpleModule module = new SimpleModule("HealthLinkLocalDateTime");
|
||||
module.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
|
||||
module.addSerializer(LocalDateTime.class, LOCAL_DATE_TIME_SERIALIZER);
|
||||
module.addSerializer(java.sql.Date.class, new ValueSerializer<java.sql.Date>() {
|
||||
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
@Override
|
||||
public void serialize(java.sql.Date value, JsonGenerator gen, SerializationContext ctx) throws JacksonException {
|
||||
gen.writeString(sdf.format(value));
|
||||
}
|
||||
});
|
||||
builder.addModule(module);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2697,21 +2697,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
log.info("getSurgeryPage 开始: orgId={}, page={}/{}, searchKey={}", organizationId, pageNo, pageSize, searchKey);
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// 无搜索时尝试从 Redis 缓存读取(手术项目变更频率低,适合缓存)
|
||||
String safeOrgId = organizationId != null ? organizationId.toString() : "";
|
||||
String cacheKey = "surgery:page:" + safeOrgId + ":" + pageNo + ":" + pageSize;
|
||||
boolean useCache = (searchKey == null || searchKey.trim().isEmpty());
|
||||
|
||||
if (useCache) {
|
||||
Object cachedObj = redisCache.getCacheObject(cacheKey);
|
||||
if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
|
||||
log.info("从 Redis 缓存获取手术项目, key: {}, records: {}", cacheKey,
|
||||
((IPage<?>) cachedObj).getRecords().size());
|
||||
return (IPage<SurgeryItemDto>) cachedObj;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 MyBatis Plus 分页查询
|
||||
IPage<SurgeryItemDto> result = doctorStationAdviceAppMapper.getSurgeryPage(
|
||||
new Page<>(pageNo, pageSize),
|
||||
PublicationStatus.ACTIVE.getValue(),
|
||||
@@ -2720,12 +2705,6 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
|
||||
|
||||
log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size());
|
||||
|
||||
// 无搜索时将结果写入缓存
|
||||
if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
|
||||
redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS);
|
||||
log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ public class SurgeryItemDto {
|
||||
@JsonProperty("unitCode_dictText")
|
||||
private String unitCodeDictText;
|
||||
|
||||
/** 拼音码(前端穿梭框本地搜索用) */
|
||||
private String pyStr;
|
||||
|
||||
/** 业务编号(前端穿梭框本地搜索用) */
|
||||
private String busNo;
|
||||
|
||||
/** 所需标本编码(来自诊疗目录配置,对应字典 specimen_code 的 dictValue) */
|
||||
private String specimenCode;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.healthlink.his.web.empi.appservice;
|
||||
|
||||
import com.healthlink.his.administration.domain.Patient;
|
||||
import com.healthlink.his.empi.domain.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IEmpiAppService {
|
||||
EmpiPerson registerPerson(EmpiPerson p);
|
||||
void mergePersons(Long primaryId, List<Long> secondaryIds);
|
||||
@@ -9,4 +12,7 @@ public interface IEmpiAppService {
|
||||
EmpiPerson findByIdCard(String idCardNo);
|
||||
List<EmpiPersonIdMapping> getMappings(String globalId);
|
||||
Map<String, Object> getStatistics();
|
||||
}
|
||||
List<Patient> findLinkedPatients(String globalId);
|
||||
List<Patient> findLinkedPatientsByIdCard(String idCardNo);
|
||||
List<EmpiPerson> listPersons(String name, String idCardNo);
|
||||
}
|
||||
@@ -1,54 +1,129 @@
|
||||
package com.healthlink.his.web.empi.appservice.impl;
|
||||
import com.healthlink.his.empi.domain.*;
|
||||
import com.healthlink.his.empi.service.*;
|
||||
import com.healthlink.his.web.empi.appservice.IEmpiAppService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.administration.domain.Patient;
|
||||
import com.healthlink.his.administration.service.IPatientService;
|
||||
import com.healthlink.his.empi.domain.EmpiPerson;
|
||||
import com.healthlink.his.empi.domain.EmpiPersonIdMapping;
|
||||
import com.healthlink.his.empi.domain.EmpiMergeLog;
|
||||
import com.healthlink.his.empi.service.IEmpiMergeLogService;
|
||||
import com.healthlink.his.empi.service.IEmpiPersonIdMappingService;
|
||||
import com.healthlink.his.empi.service.IEmpiPersonService;
|
||||
import com.healthlink.his.web.empi.appservice.IEmpiAppService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class EmpiAppServiceImpl implements IEmpiAppService {
|
||||
@Autowired private IEmpiPersonService personService;
|
||||
@Autowired private IEmpiPersonIdMappingService mappingService;
|
||||
@Autowired private IPatientService patientService;
|
||||
@Autowired private IEmpiMergeLogService mergeLogService;
|
||||
|
||||
@Override
|
||||
public EmpiPerson registerPerson(EmpiPerson p) {
|
||||
p.setGlobalId(UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase());
|
||||
p.setMergeStatus("ACTIVE"); personService.save(p); return p;
|
||||
p.setMergeStatus("ACTIVE");
|
||||
personService.save(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mergePersons(Long primaryId, List<Long> secondaryIds) {
|
||||
EmpiPerson primary = personService.getById(primaryId);
|
||||
for (Long secId : secondaryIds) {
|
||||
EmpiPerson sec = personService.getById(secId);
|
||||
if (sec == null) continue;
|
||||
List<EmpiPersonIdMapping> mappings = mappingService.list(new LambdaQueryWrapper<EmpiPersonIdMapping>()
|
||||
.eq(EmpiPersonIdMapping::getGlobalId, personService.getById(secId).getGlobalId()));
|
||||
.eq(EmpiPersonIdMapping::getGlobalId, sec.getGlobalId()));
|
||||
for (EmpiPersonIdMapping m : mappings) {
|
||||
m.setGlobalId(primary.getGlobalId());
|
||||
mappingService.updateById(m);
|
||||
}
|
||||
EmpiPerson sec = personService.getById(secId);
|
||||
sec.setMergeStatus("MERGED"); personService.updateById(sec);
|
||||
sec.setMergeStatus("MERGED");
|
||||
personService.updateById(sec);
|
||||
|
||||
EmpiMergeLog logRecord = new EmpiMergeLog();
|
||||
logRecord.setSourcePatientId(primaryId);
|
||||
logRecord.setTargetPatientId(secId);
|
||||
logRecord.setMergeType("MERGE");
|
||||
logRecord.setMergeReason("EMPI合并");
|
||||
logRecord.setMergeBy("system");
|
||||
logRecord.setMergeTime(new java.util.Date());
|
||||
logRecord.setStatus("MERGED");
|
||||
mergeLogService.save(logRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmpiPerson findByGlobalId(String globalId) {
|
||||
return personService.getOne(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getGlobalId, globalId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmpiPerson findByIdCard(String idCardNo) {
|
||||
return personService.getOne(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getIdCardNo, idCardNo));
|
||||
return personService.getOne(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getIdCardNo, idCardNo).last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EmpiPersonIdMapping> getMappings(String globalId) {
|
||||
return mappingService.list(new LambdaQueryWrapper<EmpiPersonIdMapping>().eq(EmpiPersonIdMapping::getGlobalId, globalId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStatistics() {
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
r.put("totalPersons", personService.count());
|
||||
r.put("activePersons", personService.count(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getMergeStatus, "ACTIVE")));
|
||||
r.put("mergedPersons", personService.count(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getMergeStatus, "MERGED")));
|
||||
long total = personService.count();
|
||||
long activeCount = personService.count(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getMergeStatus, "ACTIVE"));
|
||||
long mergedCount = personService.count(new LambdaQueryWrapper<EmpiPerson>().eq(EmpiPerson::getMergeStatus, "MERGED"));
|
||||
r.put("totalPersons", total);
|
||||
r.put("activePersons", activeCount);
|
||||
r.put("mergedPersons", mergedCount);
|
||||
r.put("totalMappings", mappingService.count());
|
||||
r.put("pendingMerges", total > 0 ? total - activeCount - mergedCount : 0);
|
||||
r.put("duplicateRate", total > 0 ? Math.round((double) mergedCount / total * 10000) / 100.0 : 0);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过全局ID查询关联的院内患者记录
|
||||
*/
|
||||
public List<Patient> findLinkedPatients(String globalId) {
|
||||
List<EmpiPersonIdMapping> mappings = mappingService.list(
|
||||
new LambdaQueryWrapper<EmpiPersonIdMapping>().eq(EmpiPersonIdMapping::getGlobalId, globalId));
|
||||
if (mappings.isEmpty()) return Collections.emptyList();
|
||||
|
||||
List<Long> patientIds = mappings.stream()
|
||||
.map(EmpiPersonIdMapping::getLocalPatientId)
|
||||
.collect(Collectors.toList());
|
||||
return patientService.listByIds(patientIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过身份证号查询关联的院内患者记录
|
||||
*/
|
||||
public List<Patient> findLinkedPatientsByIdCard(String idCardNo) {
|
||||
EmpiPerson person = findByIdCard(idCardNo);
|
||||
if (person == null) return Collections.emptyList();
|
||||
return findLinkedPatients(person.getGlobalId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询EMPI患者列表
|
||||
*/
|
||||
@Override
|
||||
public List<EmpiPerson> listPersons(String name, String idCardNo) {
|
||||
LambdaQueryWrapper<EmpiPerson> wrapper = new LambdaQueryWrapper<>();
|
||||
if (name != null && !name.isEmpty()) {
|
||||
wrapper.like(EmpiPerson::getName, name);
|
||||
}
|
||||
if (idCardNo != null && !idCardNo.isEmpty()) {
|
||||
wrapper.like(EmpiPerson::getIdCardNo, idCardNo);
|
||||
}
|
||||
wrapper.orderByDesc(EmpiPerson::getId);
|
||||
return personService.list(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
package com.healthlink.his.web.empi.controller;
|
||||
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.healthlink.his.empi.domain.*;
|
||||
import com.healthlink.his.web.empi.appservice.IEmpiAppService;
|
||||
@@ -7,19 +8,69 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
@Tag(name = "患者主索引(EMPI)") @RestController @RequestMapping("/api/v1/empi")
|
||||
|
||||
@Tag(name = "患者主索引(EMPI)")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/empi")
|
||||
public class EmpiController {
|
||||
@Autowired private IEmpiAppService empiAppService;
|
||||
@Operation(summary = "注册患者") @PostMapping("/person")
|
||||
public AjaxResult register(@RequestBody EmpiPerson p) { return AjaxResult.success(empiAppService.registerPerson(p)); }
|
||||
@Operation(summary = "合并患者") @PostMapping("/merge")
|
||||
public AjaxResult merge(@RequestParam Long primaryId, @RequestParam List<Long> secondaryIds) { empiAppService.mergePersons(primaryId, secondaryIds); return AjaxResult.success(); }
|
||||
@Operation(summary = "按全局ID查询") @GetMapping("/person/global/{globalId}")
|
||||
public AjaxResult findByGlobalId(@PathVariable String globalId) { return AjaxResult.success(empiAppService.findByGlobalId(globalId)); }
|
||||
@Operation(summary = "按身份证查询") @GetMapping("/person/idcard/{idCardNo}")
|
||||
public AjaxResult findByIdCard(@PathVariable String idCardNo) { return AjaxResult.success(empiAppService.findByIdCard(idCardNo)); }
|
||||
@Operation(summary = "ID映射") @GetMapping("/mappings/{globalId}")
|
||||
public AjaxResult mappings(@PathVariable String globalId) { return AjaxResult.success(empiAppService.getMappings(globalId)); }
|
||||
@Operation(summary = "统计") @GetMapping("/statistics")
|
||||
public AjaxResult statistics() { return AjaxResult.success(empiAppService.getStatistics()); }
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private IEmpiAppService empiAppService;
|
||||
|
||||
@Operation(summary = "注册患者")
|
||||
@PostMapping("/person")
|
||||
public AjaxResult register(@RequestBody EmpiPerson p) {
|
||||
return AjaxResult.success(empiAppService.registerPerson(p));
|
||||
}
|
||||
|
||||
@Operation(summary = "合并患者")
|
||||
@PostMapping("/merge")
|
||||
public AjaxResult merge(@RequestParam Long primaryId, @RequestParam List<Long> secondaryIds) {
|
||||
empiAppService.mergePersons(primaryId, secondaryIds);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "按全局ID查询EMPI")
|
||||
@GetMapping("/person/global/{globalId}")
|
||||
public AjaxResult findByGlobalId(@PathVariable String globalId) {
|
||||
return AjaxResult.success(empiAppService.findByGlobalId(globalId));
|
||||
}
|
||||
|
||||
@Operation(summary = "按身份证查询EMPI")
|
||||
@GetMapping("/person/idcard/{idCardNo}")
|
||||
public AjaxResult findByIdCard(@PathVariable String idCardNo) {
|
||||
return AjaxResult.success(empiAppService.findByIdCard(idCardNo));
|
||||
}
|
||||
|
||||
@Operation(summary = "ID映射")
|
||||
@GetMapping("/mappings/{globalId}")
|
||||
public AjaxResult mappings(@PathVariable String globalId) {
|
||||
return AjaxResult.success(empiAppService.getMappings(globalId));
|
||||
}
|
||||
|
||||
@Operation(summary = "统计")
|
||||
@GetMapping("/statistics")
|
||||
public AjaxResult statistics() {
|
||||
return AjaxResult.success(empiAppService.getStatistics());
|
||||
}
|
||||
|
||||
@Operation(summary = "通过全局ID查询关联的院内患者记录")
|
||||
@GetMapping("/linked-patients/global/{globalId}")
|
||||
public AjaxResult findLinkedPatientsByGlobalId(@PathVariable String globalId) {
|
||||
return AjaxResult.success(empiAppService.findLinkedPatients(globalId));
|
||||
}
|
||||
|
||||
@Operation(summary = "通过身份证号查询关联的院内患者记录")
|
||||
@GetMapping("/linked-patients/idcard/{idCardNo}")
|
||||
public AjaxResult findLinkedPatientsByIdCard(@PathVariable String idCardNo) {
|
||||
return AjaxResult.success(empiAppService.findLinkedPatientsByIdCard(idCardNo));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询EMPI患者列表")
|
||||
@GetMapping("/persons")
|
||||
public AjaxResult listPersons(
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String idCardNo) {
|
||||
return AjaxResult.success(empiAppService.listPersons(name, idCardNo));
|
||||
}
|
||||
}
|
||||
@@ -378,10 +378,21 @@ public class ATDManageAppServiceImpl implements IATDManageAppService {
|
||||
*/
|
||||
@Override
|
||||
public R<?> getInPatientPendingList(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize) {
|
||||
// 提取deadline手动处理,防止自动拼接列名不存在的错误
|
||||
String deadline = inpatientAdviceParam.getDeadline();
|
||||
inpatientAdviceParam.setDeadline(null);
|
||||
// 构建查询条件
|
||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
||||
|
||||
// 手动拼接截止时间条件:request_time <= deadline
|
||||
if (StringUtils.isNotEmpty(deadline)) {
|
||||
Date deadlineDate = DateUtils.parseDate(deadline);
|
||||
if (deadlineDate != null) {
|
||||
queryWrapper.le("request_time", deadlineDate);
|
||||
}
|
||||
}
|
||||
|
||||
// 患者医嘱分页列表
|
||||
Page<InpatientAdviceDto> inpatientAdvicePage = atdManageAppMapper.selectInpatientAdvicePage(
|
||||
new Page<>(pageNo, pageSize), queryWrapper, CommonConstants.TableName.MED_MEDICATION_REQUEST,
|
||||
|
||||
@@ -53,7 +53,9 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
@@ -182,18 +184,22 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
// 提取requestStatus手动处理,支持COMPLETED(3)和CHECK_VERIFIED(10)同时查询
|
||||
Integer requestStatus = inpatientAdviceParam.getRequestStatus();
|
||||
inpatientAdviceParam.setRequestStatus(null);
|
||||
// deadline 不在 UNION 子查询结果列中,且不映射为查询过滤条件
|
||||
// 原因:end_time 是医嘱结束时间,长期医嘱的 end_time 远在 deadline 之后,
|
||||
// 使用 <= 过滤会排除所有长期医嘱,导致"未校对"tab 查询为空
|
||||
// 提取deadline手动处理,需要做NULL-safe的end_time比较(Bug #763修复)
|
||||
String deadline = inpatientAdviceParam.getDeadline();
|
||||
inpatientAdviceParam.setDeadline(null);
|
||||
// 构建查询条件
|
||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
||||
|
||||
// 手动拼接requestStatus条件:COMPLETED(3)时同时包含CHECK_VERIFIED(10)和PENDING_RECEIVE(11)
|
||||
// 手动拼接requestStatus条件:
|
||||
// 1. ACTIVE(2)时:同时包含已停嘱待核对的 PENDING_STOP(13)
|
||||
// 2. COMPLETED(3)时:同时包含已校对检查 CHECK_VERIFIED(10) 和已接收 PENDING_RECEIVE(11)
|
||||
// UNION查询外层列名为request_status(T1.status_enum AS request_status),不是status_enum
|
||||
if (requestStatus != null) {
|
||||
if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
|
||||
if (RequestStatus.ACTIVE.getValue().equals(requestStatus)) {
|
||||
queryWrapper.in("request_status",
|
||||
RequestStatus.ACTIVE.getValue(), RequestStatus.PENDING_STOP.getValue());
|
||||
} else if (RequestStatus.COMPLETED.getValue().equals(requestStatus)) {
|
||||
queryWrapper.in("request_status",
|
||||
RequestStatus.COMPLETED.getValue(), RequestStatus.CHECK_VERIFIED.getValue(),
|
||||
RequestStatus.PENDING_RECEIVE.getValue());
|
||||
@@ -208,6 +214,18 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
||||
}
|
||||
// 手动拼接deadline条件:end_time IS NULL OR end_time <= deadline(Bug #763修复)
|
||||
// 住院医嘱的effective_dose_end可能为NULL(签发临时医嘱时未设置结束时间),
|
||||
// PostgreSQL中 NULL <= anything 结果为FALSE,需要先判断IS NULL
|
||||
if (deadline != null && !deadline.isEmpty()) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
Date deadlineTime = sdf.parse(deadline);
|
||||
queryWrapper.and(w -> w.isNull("end_time").or().le("end_time", deadlineTime));
|
||||
} catch (java.text.ParseException e) {
|
||||
// deadline解析失败,忽略此条件
|
||||
}
|
||||
}
|
||||
// 患者医嘱分页列表
|
||||
Page<InpatientAdviceDto> inpatientAdvicePage
|
||||
= adviceProcessAppMapper.selectInpatientAdvicePage(new Page<>(pageNo, pageSize), queryWrapper,
|
||||
@@ -348,7 +366,15 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
|
||||
// 根据执行状态过滤医嘱列表
|
||||
List<InpatientAdviceDto> filteredList = new ArrayList<>();
|
||||
if (EventStatus.COMPLETED.getValue().equals(exeStatus)) {
|
||||
if (EventStatus.PREPARATION.getValue().equals(exeStatus)) {
|
||||
// 待执行 - 过滤出没有已执行记录的医嘱(Bug #663修复)
|
||||
filteredList = inpatientAdviceList.stream().filter(
|
||||
advice -> advice.getExePerformRecordList() == null || advice.getExePerformRecordList().isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
// 更新分页数据
|
||||
inpatientAdvicePage.setRecords(filteredList);
|
||||
inpatientAdvicePage.setTotal(filteredList.size());
|
||||
} else if (EventStatus.COMPLETED.getValue().equals(exeStatus)) {
|
||||
// 已执行 - 过滤出有执行记录的医嘱
|
||||
filteredList = inpatientAdviceList.stream().filter(
|
||||
advice -> advice.getExePerformRecordList() != null && !advice.getExePerformRecordList().isEmpty())
|
||||
@@ -401,14 +427,30 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
Date checkDate = new Date();
|
||||
if (!serviceRequestList.isEmpty()) {
|
||||
List<Long> serviceReqIds = serviceRequestList.stream().map(PerformInfoDto::getRequestId).toList();
|
||||
// 先查询服务请求,按 categoryEnum 分流:检查类(23)走 CHECK_VERIFIED,其余走 COMPLETED
|
||||
// 先查询服务请求,进行状态分流:
|
||||
// 1. 如果是停嘱待核对(PENDING_STOP=13),则核对后转为停止(STOPPED=6)
|
||||
// 2. 否则按类别分流:检查类(23)走 CHECK_VERIFIED,其余走 COMPLETED
|
||||
List<ServiceRequest> allServiceRequests = serviceRequestService.listByIds(serviceReqIds);
|
||||
List<Long> stopReqIds = allServiceRequests.stream()
|
||||
.filter(sr -> RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum()))
|
||||
.map(ServiceRequest::getId).toList();
|
||||
List<Long> checkReqIds = allServiceRequests.stream()
|
||||
.filter(sr -> ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
|
||||
.filter(sr -> !RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum())
|
||||
&& ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
|
||||
.map(ServiceRequest::getId).toList();
|
||||
List<Long> otherReqIds = allServiceRequests.stream()
|
||||
.filter(sr -> !ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
|
||||
.filter(sr -> !RequestStatus.PENDING_STOP.getValue().equals(sr.getStatusEnum())
|
||||
&& !ActivityDefCategory.TEST.getValue().equals(sr.getCategoryEnum()))
|
||||
.map(ServiceRequest::getId).toList();
|
||||
|
||||
// 停嘱待核对 → 停止(STOPPED=6)
|
||||
if (!stopReqIds.isEmpty()) {
|
||||
serviceRequestService.update(new LambdaUpdateWrapper<ServiceRequest>()
|
||||
.in(ServiceRequest::getId, stopReqIds)
|
||||
.set(ServiceRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
|
||||
.set(ServiceRequest::getPerformerCheckId, practitionerId)
|
||||
.set(ServiceRequest::getCheckTime, checkDate));
|
||||
}
|
||||
// 检查类 → 已校对(CHECK_VERIFIED=10)
|
||||
if (!checkReqIds.isEmpty()) {
|
||||
serviceRequestService.updateCheckVerifiedStatus(checkReqIds, practitionerId, checkDate);
|
||||
@@ -419,24 +461,63 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
}
|
||||
// 处理转科/出院等特殊医嘱
|
||||
for (ServiceRequest serviceRequest : allServiceRequests) {
|
||||
// Bug #718: 延迟状态变更时机。核对通过时不立即变更患者就诊状态,
|
||||
// 而是等到护士在【入出转管理】中手动点击“转科”或“清床”时再处理。
|
||||
// 这样可以确保护士在真正转出前,依然能在在科列表中选中患者并处理遗留医嘱。
|
||||
if (ActivityDefCategory.TRANSFER.getValue().equals(serviceRequest.getCategoryEnum())) {
|
||||
encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
|
||||
EncounterZyStatus.PENDING_TRANSFER.getValue());
|
||||
// encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
|
||||
// EncounterZyStatus.PENDING_TRANSFER.getValue());
|
||||
} else if (ActivityDefCategory.DISCHARGE.getValue().equals(serviceRequest.getCategoryEnum())) {
|
||||
encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
|
||||
EncounterZyStatus.AWAITING_DISCHARGE.getValue());
|
||||
// encounterService.updateEncounterStatus(serviceRequest.getEncounterId(),
|
||||
// EncounterZyStatus.AWAITING_DISCHARGE.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!medRequestList.isEmpty()) {
|
||||
// 更新药品请求状态已完成
|
||||
medicationRequestService.updateCompletedStatusBatch(
|
||||
medRequestList.stream().map(PerformInfoDto::getRequestId).toList(), practitionerId, checkDate);
|
||||
List<Long> medReqIds = medRequestList.stream().map(PerformInfoDto::getRequestId).toList();
|
||||
List<MedicationRequest> allMedRequests = medicationRequestService.listByIds(medReqIds);
|
||||
|
||||
List<Long> stopMedIds = allMedRequests.stream()
|
||||
.filter(mr -> RequestStatus.PENDING_STOP.getValue().equals(mr.getStatusEnum()))
|
||||
.map(MedicationRequest::getId).toList();
|
||||
List<Long> otherMedIds = allMedRequests.stream()
|
||||
.filter(mr -> !RequestStatus.PENDING_STOP.getValue().equals(mr.getStatusEnum()))
|
||||
.map(MedicationRequest::getId).toList();
|
||||
|
||||
// 停嘱待核对 → 停止(STOPPED=6)
|
||||
if (!stopMedIds.isEmpty()) {
|
||||
medicationRequestService.update(new LambdaUpdateWrapper<MedicationRequest>()
|
||||
.in(MedicationRequest::getId, stopMedIds)
|
||||
.set(MedicationRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
|
||||
.set(MedicationRequest::getPerformerCheckId, practitionerId)
|
||||
.set(MedicationRequest::getCheckTime, checkDate));
|
||||
}
|
||||
// 其他类 → 已完成(COMPLETED=3)
|
||||
if (!otherMedIds.isEmpty()) {
|
||||
medicationRequestService.updateCompletedStatusBatch(otherMedIds, practitionerId, checkDate);
|
||||
}
|
||||
}
|
||||
if (!deviceRequestList.isEmpty()) {
|
||||
// 更新耗材请求状态已完成
|
||||
deviceRequestService.updateCompletedStatusBatch(
|
||||
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList());
|
||||
List<Long> devReqIds = deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList();
|
||||
// 同样处理耗材的停嘱核对
|
||||
List<DeviceRequest> allDevRequests = deviceRequestService.listByIds(devReqIds);
|
||||
List<Long> stopDevIds = allDevRequests.stream()
|
||||
.filter(dr -> RequestStatus.PENDING_STOP.getValue().equals(dr.getStatusEnum()))
|
||||
.map(DeviceRequest::getId).toList();
|
||||
List<Long> otherDevIds = allDevRequests.stream()
|
||||
.filter(dr -> !RequestStatus.PENDING_STOP.getValue().equals(dr.getStatusEnum()))
|
||||
.map(DeviceRequest::getId).toList();
|
||||
|
||||
if (!stopDevIds.isEmpty()) {
|
||||
deviceRequestService.update(new LambdaUpdateWrapper<DeviceRequest>()
|
||||
.in(DeviceRequest::getId, stopDevIds)
|
||||
.set(DeviceRequest::getStatusEnum, RequestStatus.STOPPED.getValue())
|
||||
.set(DeviceRequest::getPerformerCheckId, practitionerId)
|
||||
.set(DeviceRequest::getCheckTime, checkDate));
|
||||
}
|
||||
if (!otherDevIds.isEmpty()) {
|
||||
deviceRequestService.updateCompletedStatusBatch(otherDevIds);
|
||||
}
|
||||
}
|
||||
return R.ok(null, "校对成功");
|
||||
}
|
||||
@@ -512,8 +593,7 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
|
||||
if (!deviceRequestList.isEmpty()) {
|
||||
// 更新耗材请求状态待发送
|
||||
deviceRequestService.updateDraftStatusBatch(
|
||||
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList(),
|
||||
practitionerId, checkDate, backReason);
|
||||
deviceRequestList.stream().map(PerformInfoDto::getRequestId).toList());
|
||||
}
|
||||
return R.ok(null, "退回成功");
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
|
||||
// 就诊ID集合
|
||||
String encounterIds = dispenseFormSearchParam.getEncounterIds();
|
||||
dispenseFormSearchParam.setEncounterIds(null);
|
||||
// 汇总单查询不适用的字段清空(汇总单表无 tcm_flag 等列,避免 SQL 报错)
|
||||
dispenseFormSearchParam.setTcmFlag(null);
|
||||
dispenseFormSearchParam.setTherapyEnum(null);
|
||||
dispenseFormSearchParam.setExeTime(null);
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<DispenseFormSearchParam> queryWrapper = HisQueryUtils.buildQueryWrapper(dispenseFormSearchParam,
|
||||
@@ -202,8 +206,8 @@ public class MedicineSummaryAppServiceImpl implements IMedicineSummaryAppService
|
||||
// 领药人
|
||||
Long receiverId = medicineSummaryParamList.get(0).getReceiverId();
|
||||
// 申请时间(优先使用前端传递的操作时间,确保与实际操作时间一致)
|
||||
Date now = medicineSummaryParamList.get(0).getDispenseTime() != null
|
||||
? medicineSummaryParamList.get(0).getDispenseTime() : DateUtils.getNowDate();
|
||||
Date now = medicineSummaryParamList.get(0).getExecuteTime() != null
|
||||
? medicineSummaryParamList.get(0).getExecuteTime() : DateUtils.getNowDate();
|
||||
// 申请人
|
||||
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
|
||||
// 药品发放id
|
||||
|
||||
@@ -235,10 +235,21 @@ public class NurseBillingAppService implements INurseBillingAppService {
|
||||
inpatientAdviceParam.setEncounterIds(null);
|
||||
Integer exeStatus = inpatientAdviceParam.getExeStatus();
|
||||
inpatientAdviceParam.setExeStatus(null);
|
||||
// 提取deadline手动处理,防止自动拼接列名不存在的错误
|
||||
String deadline = inpatientAdviceParam.getDeadline();
|
||||
inpatientAdviceParam.setDeadline(null);
|
||||
// 构建查询条件
|
||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
||||
|
||||
// 手动拼接截止时间条件:request_time <= deadline
|
||||
if (StringUtils.isNotEmpty(deadline)) {
|
||||
Date deadlineDate = DateUtils.parseDate(deadline);
|
||||
if (deadlineDate != null) {
|
||||
queryWrapper.le("request_time", deadlineDate);
|
||||
}
|
||||
}
|
||||
|
||||
// 手动拼接住院患者id条件
|
||||
if (encounterIds != null && !encounterIds.isEmpty()) {
|
||||
List<Long> encounterIdList
|
||||
|
||||
@@ -22,10 +22,10 @@ import java.util.Date;
|
||||
public class MedicineSummaryParam {
|
||||
|
||||
/**
|
||||
* 领药时间
|
||||
* 实际执行时间
|
||||
*/
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date dispenseTime;
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date executeTime;
|
||||
|
||||
/**
|
||||
* 发放id
|
||||
|
||||
@@ -29,7 +29,9 @@ import com.healthlink.his.web.patientmanage.dto.PatientBaseInfoDto;
|
||||
import com.healthlink.his.web.patientmanage.dto.PatientIdInfoDto;
|
||||
import com.healthlink.his.web.patientmanage.dto.PatientInfoInitDto;
|
||||
import com.healthlink.his.web.patientmanage.mapper.PatientManageMapper;
|
||||
import com.healthlink.his.empi.event.PatientSavedEvent;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -54,6 +56,9 @@ public class PatientInformationServiceImpl implements IPatientInformationService
|
||||
@Autowired
|
||||
PatientManageMapper patientManageMapper;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired
|
||||
private IPatientService patientService;
|
||||
|
||||
@@ -379,9 +384,13 @@ public class PatientInformationServiceImpl implements IPatientInformationService
|
||||
.set(Patient::getEducationLevel, patient.getEducationLevel())
|
||||
.set(Patient::getCompanyAddress, patient.getCompanyAddress());
|
||||
patientService.update(updateWrapper);
|
||||
// 发布患者保存事件,触发EMPI同步
|
||||
eventPublisher.publishEvent(new PatientSavedEvent(this, patient, false));
|
||||
} else {
|
||||
// 新增操作
|
||||
patientService.save(patient);
|
||||
// 发布患者保存事件,触发EMPI同步
|
||||
eventPublisher.publishEvent(new PatientSavedEvent(this, patient, true));
|
||||
}
|
||||
|
||||
return patient;
|
||||
|
||||
@@ -970,7 +970,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
// }
|
||||
// æ ¹æ®çœå¸‚医ä¿åˆ†ç»„
|
||||
Map<String, List<PaymentRecDetailAccountResult>> paymentDetailsMapByContract = PaymentRecDetailAccountResultList
|
||||
.stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
|
||||
.stream()
|
||||
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
|
||||
|
||||
// 查询所有的收费项
|
||||
List<String> chargeItemIdStrs = paymentReconciliationList.stream().map(PaymentReconciliation::getChargeItemIds)
|
||||
@@ -1043,7 +1045,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
// é•¿å¤§ç‰ˆæœ¬è¦æ˜¾ç¤ºå‡ºæ¥çœå¸‚医ä¿çš„区别
|
||||
List<Contract> redisContractList = iContractService.getRedisContractList();
|
||||
Map<String, List<Contract>> contractMapByBusNo
|
||||
= redisContractList.stream().collect(Collectors.groupingBy(Contract::getBusNo));
|
||||
= redisContractList.stream()
|
||||
.filter(e -> e.getBusNo() != null && !e.getBusNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(Contract::getBusNo));
|
||||
for (Map.Entry<String, List<PaymentRecDetailAccountResult>> stringListEntry : paymentDetailsMapByContract
|
||||
.entrySet()) {
|
||||
String key = stringListEntry.getKey();
|
||||
@@ -1445,7 +1449,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
// 医ä¿äººæ¬¡è‡ªè´¹äººæ¬¡è®¡ç®—
|
||||
List<EncounterAccountDto> list = iEncounterService.getEncounterInfoByTime(startDate, endDate);
|
||||
Map<String, List<EncounterAccountDto>> encounterAccountDtoMapByContract
|
||||
= list.stream().collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
|
||||
= list.stream()
|
||||
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
|
||||
for (Map.Entry<String, List<EncounterAccountDto>> stringListEntry : encounterAccountDtoMapByContract
|
||||
.entrySet()) {
|
||||
String key = stringListEntry.getKey();
|
||||
@@ -1519,7 +1525,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
|
||||
// æ ¹æ®çœå¸‚医ä¿åˆ†ç»„
|
||||
Map<String, List<PaymentRecDetailAccountResult>> paymentDetailsMapByContract = PaymentRecDetailAccountResultList
|
||||
.stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
|
||||
.stream()
|
||||
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));
|
||||
|
||||
BigDecimal cashSum = BigDecimal.ZERO;// 现金总数 = rmbCashSum + vxCashSum + aliCashSum + uniCashSum
|
||||
BigDecimal rmbCashSum = BigDecimal.ZERO;// 现金总数
|
||||
@@ -1535,7 +1543,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
// é•¿å¤§ç‰ˆæœ¬è¦æ˜¾ç¤ºå‡ºæ¥çœå¸‚医ä¿çš„区别
|
||||
List<Contract> redisContractList = iContractService.getRedisContractList();
|
||||
Map<String, List<Contract>> contractMapByBusNo
|
||||
= redisContractList.stream().collect(Collectors.groupingBy(Contract::getBusNo));
|
||||
= redisContractList.stream()
|
||||
.filter(e -> e.getBusNo() != null && !e.getBusNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(Contract::getBusNo));
|
||||
for (Map.Entry<String, List<PaymentRecDetailAccountResult>> stringListEntry : paymentDetailsMapByContract
|
||||
.entrySet()) {
|
||||
|
||||
@@ -1922,7 +1932,9 @@ public class IChargeBillServiceImpl implements IChargeBillService {
|
||||
// 医ä¿äººæ¬¡è‡ªè´¹äººæ¬¡è®¡ç®—
|
||||
List<EncounterAccountDto> list = iEncounterService.getEncounterInfoByTime(startDate, endDate);
|
||||
Map<String, List<EncounterAccountDto>> encounterAccountDtoMapByContract
|
||||
= list.stream().collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
|
||||
= list.stream()
|
||||
.filter(e -> e.getContractNo() != null && !e.getContractNo().isEmpty())
|
||||
.collect(Collectors.groupingBy(EncounterAccountDto::getContractNo));
|
||||
for (Map.Entry<String, List<EncounterAccountDto>> stringListEntry : encounterAccountDtoMapByContract
|
||||
.entrySet()) {
|
||||
String key = stringListEntry.getKey();
|
||||
|
||||
@@ -322,33 +322,60 @@ public class AdviceManageAppServiceImpl implements IAdviceManageAppService {
|
||||
|
||||
try {
|
||||
// 根据adviceType判断删除哪种类型的医嘱
|
||||
if (ItemType.MEDICINE.getValue().equals(adviceType)) {
|
||||
// 药品删除
|
||||
iMedicationRequestService.removeById(requestId);
|
||||
iMedicationDispenseService.deleteMedicationDispense(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.MED_MEDICATION_REQUEST, requestId);
|
||||
log.info("删除药品医嘱成功,requestId: {}", requestId);
|
||||
if (ItemType.MEDICINE.getValue().equals(adviceType) || (adviceType != null && adviceType == 7)) {
|
||||
// 药品删除 (7表示出院带药,也按药品逻辑删除)
|
||||
MedicationRequest medRequest = iMedicationRequestService.getById(requestId);
|
||||
if (medRequest != null) {
|
||||
if (!RequestStatus.DRAFT.getValue().equals(medRequest.getStatusEnum())) {
|
||||
throw new ServiceException("仅待签发状态的医嘱允许删除");
|
||||
}
|
||||
iMedicationRequestService.removeById(requestId);
|
||||
iMedicationDispenseService.deleteMedicationDispense(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.MED_MEDICATION_REQUEST, requestId);
|
||||
log.info("删除药品医嘱成功,requestId: {}", requestId);
|
||||
}
|
||||
|
||||
} else if (ItemType.DEVICE.getValue().equals(adviceType)) {
|
||||
// 耗材删除
|
||||
iDeviceRequestService.removeById(requestId);
|
||||
iDeviceDispenseService.deleteDeviceDispense(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_DEVICE_REQUEST, requestId);
|
||||
log.info("删除耗材医嘱成功,requestId: {}", requestId);
|
||||
DeviceRequest deviceRequest = iDeviceRequestService.getById(requestId);
|
||||
if (deviceRequest != null) {
|
||||
if (!RequestStatus.DRAFT.getValue().equals(deviceRequest.getStatusEnum())) {
|
||||
throw new ServiceException("仅待签发状态的医嘱允许删除");
|
||||
}
|
||||
iDeviceRequestService.removeById(requestId);
|
||||
iDeviceDispenseService.deleteDeviceDispense(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_DEVICE_REQUEST, requestId);
|
||||
log.info("删除耗材医嘱成功,requestId: {}", requestId);
|
||||
}
|
||||
|
||||
} else if (ItemType.ACTIVITY.getValue().equals(adviceType)
|
||||
|| (adviceType != null && (adviceType == 26 || adviceType == 31))) {
|
||||
// 诊疗活动删除(包括护理type=26和未知类型type=31)
|
||||
iServiceRequestService.removeById(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_SERVICE_REQUEST, requestId);
|
||||
log.info("删除诊疗/护理医嘱成功,requestId: {}, adviceType: {}", requestId, adviceType);
|
||||
ServiceRequest serviceRequest = iServiceRequestService.getById(requestId);
|
||||
if (serviceRequest != null) {
|
||||
if (!RequestStatus.DRAFT.getValue().equals(serviceRequest.getStatusEnum())) {
|
||||
throw new ServiceException("仅待签发状态的医嘱允许删除");
|
||||
}
|
||||
iServiceRequestService.removeById(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_SERVICE_REQUEST, requestId);
|
||||
log.info("删除诊疗/护理医嘱成功,requestId: {}, adviceType: {}", requestId, adviceType);
|
||||
}
|
||||
|
||||
} else {
|
||||
// 未知类型,尝试按诊疗活动删除(兜底策略)
|
||||
log.warn("未知的adviceType: {},尝试按诊疗活动删除,requestId: {}", adviceType, requestId);
|
||||
ServiceRequest serviceRequest = iServiceRequestService.getById(requestId);
|
||||
if (serviceRequest != null) {
|
||||
if (!RequestStatus.DRAFT.getValue().equals(serviceRequest.getStatusEnum())) {
|
||||
throw new ServiceException("仅待签发状态的医嘱允许删除");
|
||||
}
|
||||
}
|
||||
iServiceRequestService.removeById(requestId);
|
||||
iChargeItemService.deleteByServiceTableAndId(CommonConstants.TableName.WOR_SERVICE_REQUEST, requestId);
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
// 业务校验异常,直接向上抛出,中断并回滚事务
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("删除医嘱失败,requestId: {}, adviceType: {}", requestId, adviceType, e);
|
||||
// 继续处理其他记录,不中断整个流程
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- V2026_0616_1: EMPI核心表 — empi_person + empi_person_id_mapping
|
||||
-- 补充 V20 中遗漏的两张EMPI核心表
|
||||
|
||||
-- 1. EMPI主索引表(全局患者主记录)
|
||||
CREATE TABLE IF NOT EXISTS empi_person (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
global_id VARCHAR(32) NOT NULL,
|
||||
id_card_no VARCHAR(20),
|
||||
patient_name VARCHAR(50),
|
||||
gender VARCHAR(10),
|
||||
birth_date DATE,
|
||||
phone VARCHAR(20),
|
||||
address TEXT,
|
||||
merge_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
source_system VARCHAR(50),
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
tenant_id INT DEFAULT 0,
|
||||
delete_flag VARCHAR(1) NOT NULL DEFAULT '0'
|
||||
);
|
||||
COMMENT ON TABLE empi_person IS 'EMPI患者主索引';
|
||||
COMMENT ON COLUMN empi_person.global_id IS '全局唯一患者ID';
|
||||
COMMENT ON COLUMN empi_person.merge_status IS '合并状态(ACTIVE/MERGED)';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_ep_global ON empi_person(global_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ep_idcard ON empi_person(id_card_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_ep_name ON empi_person(patient_name);
|
||||
|
||||
-- 2. EMPI ID映射表(全局ID与院内系统患者ID的映射关系)
|
||||
CREATE TABLE IF NOT EXISTS empi_person_id_mapping (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
global_id VARCHAR(32) NOT NULL,
|
||||
local_patient_id BIGINT NOT NULL,
|
||||
source_system VARCHAR(50) NOT NULL,
|
||||
id_type VARCHAR(20) NOT NULL,
|
||||
id_value VARCHAR(100) NOT NULL,
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
tenant_id INT DEFAULT 0,
|
||||
delete_flag VARCHAR(1) NOT NULL DEFAULT '0'
|
||||
);
|
||||
COMMENT ON TABLE empi_person_id_mapping IS 'EMPI患者ID映射表';
|
||||
COMMENT ON COLUMN empi_person_id_mapping.global_id IS 'EMPI全局患者ID';
|
||||
COMMENT ON COLUMN empi_person_id_mapping.local_patient_id IS '院内患者ID';
|
||||
COMMENT ON COLUMN empi_person_id_mapping.source_system IS '来源系统编码';
|
||||
COMMENT ON COLUMN empi_person_id_mapping.id_type IS '标识类型(MRN/INSURANCE/CARD等)';
|
||||
COMMENT ON COLUMN empi_person_id_mapping.id_value IS '标识值';
|
||||
CREATE INDEX IF NOT EXISTS idx_epim_global ON empi_person_id_mapping(global_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_epim_local ON empi_person_id_mapping(local_patient_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_epim_source ON empi_person_id_mapping(source_system);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE empi_merge_log ADD COLUMN IF NOT EXISTS create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- DEPRECATED: 本迁移已迁移至 V45__bug745_fix_mr_sealing_medical_record_id.sql
|
||||
-- 原因:与 V42__add_delete_flag_columns.sql 版本号重复,导致 Flyway 阻塞
|
||||
-- 此文件保留为空操作以避免 Flyway 校验错误
|
||||
@@ -0,0 +1,22 @@
|
||||
-- V45: Create lab_activity_def_device_def table
|
||||
-- 检验活动定义与耗材/设备定义关联表
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lab_activity_def_device_def (
|
||||
id BIGINT PRIMARY KEY,
|
||||
activity_definition_id BIGINT,
|
||||
device_definition_id BIGINT,
|
||||
device_definition_name VARCHAR(255),
|
||||
instrument_id BIGINT,
|
||||
instrument_name VARCHAR(255),
|
||||
device_quantity INTEGER DEFAULT 0,
|
||||
delete_flag VARCHAR(1) DEFAULT '0',
|
||||
activity_definition_name VARCHAR(255),
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP,
|
||||
tenant_id INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lab_act_dev_act_def_id ON lab_activity_def_device_def(activity_definition_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lab_act_dev_tenant ON lab_activity_def_device_def(tenant_id);
|
||||
@@ -44,6 +44,7 @@
|
||||
ON T1.organization_id = T4.id
|
||||
AND T4.delete_flag = '0'
|
||||
WHERE T1.delete_flag = '0'
|
||||
AND T1.status_enum IN (4, 5)
|
||||
GROUP BY T1.id,
|
||||
T1.bus_no,
|
||||
T1.patient_id,
|
||||
|
||||
@@ -903,7 +903,9 @@
|
||||
t2.ID AS charge_item_definition_id,
|
||||
t2.price AS price,
|
||||
t1.permitted_unit_code AS unit_code,
|
||||
COALESCE(sdd.dict_label, t1.permitted_unit_code) AS unit_code_dict_text
|
||||
COALESCE(sdd.dict_label, t1.permitted_unit_code) AS unit_code_dict_text,
|
||||
t1.py_str AS py_str,
|
||||
t1.bus_no AS bus_no
|
||||
FROM wor_activity_definition t1
|
||||
LEFT JOIN adm_charge_item_definition t2
|
||||
ON t2.instance_id = t1.ID
|
||||
@@ -915,6 +917,7 @@
|
||||
AND sdd.dict_type = 'unit_code'
|
||||
AND sdd.status = '0'
|
||||
WHERE t1.delete_flag = '0'
|
||||
AND t1.status_enum = #{statusEnum}
|
||||
AND (t1.category_code = '手术' OR t1.category_code = '24')
|
||||
<if test="searchKey != null and searchKey != ''">
|
||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||
@@ -949,6 +952,7 @@
|
||||
AND sdd.dict_type = 'unit_code'
|
||||
AND sdd.status = '0'
|
||||
WHERE t1.delete_flag = '0'
|
||||
AND t1.status_enum = #{statusEnum}
|
||||
AND t1.category_code = #{categoryCode}
|
||||
<if test="searchKey != null and searchKey != ''">
|
||||
AND (t1.name ILIKE '%' || #{searchKey} || '%' OR t1.py_str ILIKE '%' || #{searchKey} || '%')
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
NULL::numeric AS unit_price,
|
||||
NULL::numeric AS total_price,
|
||||
NULL::bigint AS stopper_id,
|
||||
NULL::varchar AS stopper_name
|
||||
CASE WHEN T1.status_enum IN (6, 13) THEN T1.update_by ELSE NULL END AS stopper_name
|
||||
FROM med_medication_request AS T1
|
||||
LEFT JOIN med_medication_definition AS T2
|
||||
ON T2.id = T1.medication_id
|
||||
@@ -331,7 +331,7 @@
|
||||
T1.unit_code AS unit_code,
|
||||
T1.status_enum AS request_status,
|
||||
NULL::varchar AS method_code,
|
||||
NULL::varchar AS rate_code,
|
||||
T1.rate_code AS rate_code,
|
||||
NULL::numeric AS dose,
|
||||
NULL::varchar AS dose_unit_code,
|
||||
ao1.id AS position_id,
|
||||
@@ -359,7 +359,7 @@
|
||||
NULL::numeric AS unit_price,
|
||||
NULL::numeric AS total_price,
|
||||
NULL::bigint AS stopper_id,
|
||||
NULL::varchar AS stopper_name
|
||||
CASE WHEN T1.status_enum IN (6, 13) THEN T1.update_by ELSE NULL END AS stopper_name
|
||||
FROM wor_service_request AS T1
|
||||
LEFT JOIN wor_activity_definition AS T2
|
||||
ON T2.id = T1.activity_id
|
||||
@@ -496,7 +496,7 @@
|
||||
NULL::numeric AS unit_price,
|
||||
NULL::numeric AS total_price,
|
||||
NULL::bigint AS stopper_id,
|
||||
NULL::varchar AS stopper_name
|
||||
CASE WHEN T1.status_enum IN (6, 13) THEN T1.update_by ELSE NULL END AS stopper_name
|
||||
FROM wor_device_request AS T1
|
||||
LEFT JOIN adm_device_definition AS T2
|
||||
ON T2.id = T1.device_def_id
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<collection property="medicineSummaryParamList" ofType="com.healthlink.his.web.inhospitalnursestation.dto.MedicineSummaryParam">
|
||||
<result property="procedureId" column="procedure_id"/>
|
||||
<result property="dispenseId" column="dispense_id"/>
|
||||
<result property="dispenseTime" column="execution_time"/>
|
||||
<result property="executeTime" column="execution_time"/>
|
||||
<result property="dispenseStatus" column="dispense_status"/>
|
||||
</collection>
|
||||
</resultMap>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.healthlink.his.empi.event;
|
||||
|
||||
import com.healthlink.his.administration.domain.Patient;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class PatientSavedEvent extends ApplicationEvent {
|
||||
private final Patient patient;
|
||||
private final boolean isNew;
|
||||
|
||||
public PatientSavedEvent(Object source, Patient patient, boolean isNew) {
|
||||
super(source);
|
||||
this.patient = patient;
|
||||
this.isNew = isNew;
|
||||
}
|
||||
|
||||
public Patient getPatient() { return patient; }
|
||||
public boolean isNew() { return isNew; }
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.healthlink.his.empi.listener;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.administration.domain.Patient;
|
||||
import com.healthlink.his.empi.domain.EmpiPerson;
|
||||
import com.healthlink.his.empi.domain.EmpiPersonIdMapping;
|
||||
import com.healthlink.his.empi.event.PatientSavedEvent;
|
||||
import com.healthlink.his.empi.service.IEmpiPersonIdMappingService;
|
||||
import com.healthlink.his.empi.service.IEmpiPersonService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmpiSyncListener {
|
||||
|
||||
@Autowired
|
||||
private IEmpiPersonService empiPersonService;
|
||||
|
||||
@Autowired
|
||||
private IEmpiPersonIdMappingService empiPersonIdMappingService;
|
||||
|
||||
@EventListener
|
||||
public void onPatientSaved(PatientSavedEvent event) {
|
||||
Patient patient = event.getPatient();
|
||||
try {
|
||||
if (event.isNew()) {
|
||||
syncNewPatient(patient);
|
||||
} else {
|
||||
syncUpdatedPatient(patient);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("EMPI同步失败, patientId={}, error={}", patient.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncNewPatient(Patient patient) {
|
||||
String idCard = patient.getIdCard();
|
||||
EmpiPerson existingPerson = null;
|
||||
if (idCard != null && !idCard.isEmpty()) {
|
||||
existingPerson = empiPersonService.getOne(
|
||||
new LambdaQueryWrapper<EmpiPerson>()
|
||||
.eq(EmpiPerson::getIdCardNo, idCard)
|
||||
.eq(EmpiPerson::getMergeStatus, "ACTIVE"));
|
||||
}
|
||||
|
||||
if (existingPerson == null) {
|
||||
EmpiPerson person = new EmpiPerson();
|
||||
person.setGlobalId(UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase());
|
||||
person.setIdCardNo(idCard);
|
||||
person.setName(patient.getName());
|
||||
person.setGender(patient.getGenderEnum() != null && patient.getGenderEnum() == 1 ? "M" : "F");
|
||||
person.setBirthDate(patient.getBirthDate());
|
||||
person.setPhone(patient.getPhone());
|
||||
person.setAddress(patient.getAddress());
|
||||
person.setMergeStatus("ACTIVE");
|
||||
person.setSourceSystem("HIS");
|
||||
empiPersonService.save(person);
|
||||
existingPerson = person;
|
||||
log.info("EMPI主索引创建: globalId={}, patientId={}", existingPerson.getGlobalId(), patient.getId());
|
||||
}
|
||||
|
||||
createMappingIfNeeded(existingPerson.getGlobalId(), patient);
|
||||
}
|
||||
|
||||
private void syncUpdatedPatient(Patient patient) {
|
||||
EmpiPersonIdMapping mapping = empiPersonIdMappingService.getOne(
|
||||
new LambdaQueryWrapper<EmpiPersonIdMapping>()
|
||||
.eq(EmpiPersonIdMapping::getLocalPatientId, patient.getId()));
|
||||
|
||||
if (mapping != null) {
|
||||
EmpiPerson person = empiPersonService.getOne(
|
||||
new LambdaQueryWrapper<EmpiPerson>()
|
||||
.eq(EmpiPerson::getGlobalId, mapping.getGlobalId()));
|
||||
if (person != null) {
|
||||
person.setName(patient.getName());
|
||||
person.setGender(patient.getGenderEnum() != null && patient.getGenderEnum() == 1 ? "M" : "F");
|
||||
person.setBirthDate(patient.getBirthDate());
|
||||
person.setPhone(patient.getPhone());
|
||||
person.setAddress(patient.getAddress());
|
||||
empiPersonService.updateById(person);
|
||||
}
|
||||
} else {
|
||||
syncNewPatient(patient);
|
||||
}
|
||||
}
|
||||
|
||||
private void createMappingIfNeeded(String globalId, Patient patient) {
|
||||
EmpiPersonIdMapping existing = empiPersonIdMappingService.getOne(
|
||||
new LambdaQueryWrapper<EmpiPersonIdMapping>()
|
||||
.eq(EmpiPersonIdMapping::getGlobalId, globalId)
|
||||
.eq(EmpiPersonIdMapping::getLocalPatientId, patient.getId()));
|
||||
|
||||
if (existing == null) {
|
||||
EmpiPersonIdMapping mapping = new EmpiPersonIdMapping();
|
||||
mapping.setGlobalId(globalId);
|
||||
mapping.setLocalPatientId(patient.getId());
|
||||
mapping.setSourceSystem("HIS");
|
||||
mapping.setIdType("MRN");
|
||||
mapping.setIdValue(patient.getBusNo() != null ? patient.getBusNo() : String.valueOf(patient.getId()));
|
||||
empiPersonIdMappingService.save(mapping);
|
||||
log.info("EMPI映射创建: globalId={}, patientId={}", globalId, patient.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取当前用户可访问的菜单树(无需管理员权限)
|
||||
export function getUserMenus() {
|
||||
return request({
|
||||
url: '/system/menu/userMenus',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询菜单列表
|
||||
export function listMenu(query) {
|
||||
return request({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="table-container">
|
||||
<div ref="tableWrapperRef" class="table-wrapper">
|
||||
<vxe-table
|
||||
@@ -9,8 +9,7 @@
|
||||
:stripe="stripe"
|
||||
:size="size === 'large' ? 'medium' : size === 'small' ? 'mini' : 'small'"
|
||||
:height="computedTableHeight"
|
||||
:row-config="{ keyField: rowKey || 'id', isHover: true }"
|
||||
:highlight-current-row="highlightCurrentRow"
|
||||
:row-config="{ keyField: rowKey || 'id', isHover: true, isCurrent: highlightCurrentRow }"
|
||||
show-overflow="title"
|
||||
show-header-overflow="title"
|
||||
:auto-resize="true"
|
||||
|
||||
@@ -67,7 +67,7 @@ const useUserStore = defineStore(
|
||||
this.avatar = avatar
|
||||
this.optionMap = res.optionMap || {}
|
||||
// 优先从optionMap获取配置,如果没有则从optionJson获取
|
||||
this.hospitalName = this.optionMap.hospitalName || res.optionJson.hospitalName || ''
|
||||
this.hospitalName = res.tenantName || this.optionMap.hospitalName || res.optionJson.hospitalName || ''
|
||||
this.tenantName = res.tenantName || ''
|
||||
|
||||
resolve(res)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import {hiprint} from 'vue-plugin-hiprint';
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
// 打印模板映射表 .
|
||||
const TEMPLATE_MAP = {
|
||||
// CLINIC_CHARGE: () => import('@/views/charge/cliniccharge/components/template.json'),
|
||||
// DISPOSAL: () => import('@/views/clinicmanagement/disposal/components/disposalTemplate.json'),
|
||||
//处方签
|
||||
PRESCRIPTION: () => import('@/components/Print/Prescription.json'),
|
||||
//处置单
|
||||
@@ -137,29 +137,53 @@ export async function simplePrintWithDialog(
|
||||
|
||||
// 导出模板名称常量
|
||||
export const PRINT_TEMPLATE = {
|
||||
// CLINIC_CHARGE: 'CLINIC_CHARGE',
|
||||
DAY_END: 'DAY_END',
|
||||
WESTERN_MEDICINE: 'WESTERN_MEDICINE',
|
||||
IN_HOSPITAL_DISPENSING: 'IN_HOSPITAL_DISPENSING',
|
||||
//门诊挂号
|
||||
OUTPATIENT_REGISTRATION: 'OUTPATIENT_REGISTRATION',
|
||||
// 门诊手术计费
|
||||
OUTPATIENT_SURGERY_CHARGE: 'OUTPATIENT_SURGERY_CHARGE',
|
||||
//门诊收费
|
||||
OUTPATIENT_CHARGE: 'OUTPATIENT_CHARGE',
|
||||
//处方签
|
||||
PRESCRIPTION: 'PRESCRIPTION',
|
||||
//处置单
|
||||
DISPOSAL: 'DISPOSAL',
|
||||
//门诊病历
|
||||
OUTPATIENT_MEDICAL_RECORD: 'OUTPATIENT_MEDICAL_RECORD',
|
||||
//门诊输液贴
|
||||
OUTPATIENT_INFUSION: 'OUTPATIENT_INFUSION',
|
||||
//手术记录
|
||||
OPERATIVE_RECORD: 'OPERATIVE_RECORD',
|
||||
//红旗门诊病历
|
||||
HQOUTPATIENT_MEDICAL_RECORD: 'HQOUTPATIENT_MEDICAL_RECORD',
|
||||
//预交金
|
||||
ADVANCE_PAYMENT: 'ADVANCE_PAYMENT',
|
||||
//中药处方单
|
||||
CHINESE_MEDICINE_PRESCRIPTION: 'CHINESE_MEDICINE_PRESCRIPTION',
|
||||
//药房处方单
|
||||
PHARMACY_PRESCRIPTION: 'PHARMACY_PRESCRIPTION',
|
||||
//中药医生处方单
|
||||
DOC_CHINESE_MEDICINE_PRESCRIPTION: 'DOC_CHINESE_MEDICINE_PRESCRIPTION',
|
||||
|
||||
// ========== 新增模板(原LODOP迁移)==========
|
||||
//腕带
|
||||
WRIST_BAND: 'WRIST_BAND',
|
||||
//分诊条
|
||||
TRIAGE_TICKET: 'TRIAGE_TICKET',
|
||||
//输液标签
|
||||
INJECT_LABEL: 'INJECT_LABEL',
|
||||
//床头卡
|
||||
BED_CARD: 'BED_CARD',
|
||||
//护理交接班
|
||||
CHANGE_SHIFT_BILL: 'CHANGE_SHIFT_BILL',
|
||||
//医嘱执行单
|
||||
EXE_ORDER_SHEET: 'EXE_ORDER_SHEET',
|
||||
//体温单
|
||||
TEMPERATURE_SHEET: 'TEMPERATURE_SHEET',
|
||||
//会诊申请单
|
||||
CONSULTATION: 'CONSULTATION',
|
||||
};
|
||||
|
||||
@@ -182,20 +206,28 @@ export function getPrinterList() {
|
||||
}
|
||||
}
|
||||
|
||||
import useUserStore from '@/store/modules/user';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
* @returns {string} 用户ID
|
||||
*/
|
||||
function getCurrentUserId() {
|
||||
try {
|
||||
// 从Pinia store中获取当前用户ID
|
||||
const userStore = useUserStore();
|
||||
return userStore.id || '';
|
||||
} catch (e) {
|
||||
console.error('获取用户ID失败:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成打印机缓存键
|
||||
* @param {string} businessName 打印业务名称
|
||||
* @returns {string} 缓存键
|
||||
*/
|
||||
function getPrinterCacheKey(businessName) {
|
||||
const userId = getCurrentUserId();
|
||||
@@ -204,6 +236,8 @@ function getPrinterCacheKey(businessName) {
|
||||
|
||||
/**
|
||||
* 从缓存获取上次选择的打印机
|
||||
* @param {string} businessName 打印业务名称
|
||||
* @returns {string} 打印机名称
|
||||
*/
|
||||
export function getCachedPrinter(businessName = 'default') {
|
||||
const cacheKey = getPrinterCacheKey(businessName);
|
||||
@@ -212,6 +246,8 @@ export function getCachedPrinter(businessName = 'default') {
|
||||
|
||||
/**
|
||||
* 保存打印机选择到缓存
|
||||
* @param {string} printerName 打印机名称
|
||||
* @param {string} businessName 打印业务名称
|
||||
*/
|
||||
export function savePrinterToCache(printerName, businessName = 'default') {
|
||||
if (printerName) {
|
||||
@@ -221,7 +257,13 @@ export function savePrinterToCache(printerName, businessName = 'default') {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行打印操作 (带自动二维码生成逻辑)
|
||||
* 执行打印操作
|
||||
* @param {Array} data 打印数据
|
||||
* @param {Object} template 打印模板
|
||||
* @param {string} printerName 打印机名称(可选)
|
||||
* @param {Object} options 打印选项(可选)
|
||||
* @param {string} businessName 打印业务名称(可选)
|
||||
* @returns {Promise} 打印结果Promise
|
||||
*/
|
||||
export function executePrint(data, template, printerName, options = {}, businessName = 'default') {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -286,6 +328,12 @@ export function executePrint(data, template, printerName, options = {}, business
|
||||
|
||||
/**
|
||||
* 选择打印机并执行打印
|
||||
* @param {Array} data 打印数据
|
||||
* @param {Object} template 打印模板
|
||||
* @param {Function} showPrinterDialog 显示打印机选择对话框的函数
|
||||
* @param {Object} modal 消息提示对象
|
||||
* @param {Function} callback 打印完成后的回调函数
|
||||
* @param {string} businessName 打印业务名称(可选)
|
||||
*/
|
||||
export async function selectPrinterAndPrint(
|
||||
data,
|
||||
@@ -296,24 +344,29 @@ export async function selectPrinterAndPrint(
|
||||
businessName = 'default'
|
||||
) {
|
||||
try {
|
||||
// 获取打印机列表
|
||||
const printerList = getPrinterList();
|
||||
|
||||
if (printerList.length === 0) {
|
||||
modal.msgWarning('未检测到可用打印机');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取缓存的打印机
|
||||
const cachedPrinter = getCachedPrinter(businessName);
|
||||
let selectedPrinter = '';
|
||||
|
||||
// 判断打印机选择逻辑
|
||||
if (printerList.length === 1) {
|
||||
selectedPrinter = printerList[0].name;
|
||||
await executePrint(data, template, selectedPrinter, {}, businessName);
|
||||
if (callback) callback();
|
||||
} else if (cachedPrinter && printerList.some((p) => p.name === cachedPrinter)) {
|
||||
} else if (cachedPrinter && printerList.some((printer) => printer.name === cachedPrinter)) {
|
||||
selectedPrinter = cachedPrinter;
|
||||
await executePrint(data, template, selectedPrinter, {}, businessName);
|
||||
if (callback) callback();
|
||||
} else {
|
||||
// 调用显示打印机选择对话框的函数
|
||||
showPrinterDialog(printerList, async (chosenPrinter) => {
|
||||
try {
|
||||
await executePrint(data, template, chosenPrinter, {}, businessName);
|
||||
@@ -324,40 +377,375 @@ export async function selectPrinterAndPrint(
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
modal.msgError(error.message || '获取打印机失败');
|
||||
modal.msgError(error.message || '获取打印机列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览打印
|
||||
*/
|
||||
// 预览打印
|
||||
export function previewPrint(elementDom) {
|
||||
if (elementDom) {
|
||||
// 初始化已在 main.js 中完成,无需重复调用
|
||||
// hiprint.init();
|
||||
const hiprintTemplate = new hiprint.PrintTemplate();
|
||||
// printByHtml为预览打印
|
||||
hiprintTemplate.printByHtml(elementDom, {});
|
||||
} else {
|
||||
ElMessage.error('加载模版失败');
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '加载模版失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打印门诊挂号收据
|
||||
* 打印门诊挂号收据(使用浏览器打印,模板与补打挂号一致)
|
||||
* @param {Object} data 打印数据
|
||||
* @param {Object} options 打印选项
|
||||
* @returns {Promise} 打印结果 Promise
|
||||
*/
|
||||
export function printRegistrationReceipt(data, options = {}) {
|
||||
// 此处保持原有的 HTML 拼接逻辑(略)
|
||||
return Promise.resolve({ success: true, message: '打印窗口已打开' });
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 构建打印内容的 HTML
|
||||
const printContent = `
|
||||
<div class="print-header">
|
||||
<div class="header-content">
|
||||
<div class="document-title">门诊预约挂号凭条</div>
|
||||
<div class="print-time">打印时间:${data.printTime || new Date().toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-section">
|
||||
<div class="section-title">患者基本信息</div>
|
||||
<div class="info-row">
|
||||
<span class="label">患者姓名:</span>
|
||||
<span class="value">${data.patientName || '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">就诊卡号:</span>
|
||||
<span class="value">${data.cardNo || data.busNo || '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">身份证号:</span>
|
||||
<span class="value">${data.idCard ? maskIdCard(data.idCard) : '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">联系电话:</span>
|
||||
<span class="value">${data.phone || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-section">
|
||||
<div class="section-title">挂号信息</div>
|
||||
<div class="info-row">
|
||||
<span class="label">就诊科室:</span>
|
||||
<span class="value">${data.organizationName || '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">医生姓名:</span>
|
||||
<span class="value">${data.practitionerName || '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">挂号类型:</span>
|
||||
<span class="value">${data.healthcareName || '-'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">挂号时间:</span>
|
||||
<span class="value">${data.visitTime || data.chargeTime || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="print-section">
|
||||
<div class="section-title">费用信息</div>
|
||||
<table class="fee-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目</th>
|
||||
<th>数量</th>
|
||||
<th>单价</th>
|
||||
<th>金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>挂号费</td>
|
||||
<td>1</td>
|
||||
<td>¥${parseFloat(data.price || 0).toFixed(2)}</td>
|
||||
<td>¥${parseFloat(data.price || 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
${parseFloat(data.activityPrice || 0) > 0 ? `
|
||||
<tr>
|
||||
<td>诊疗费</td>
|
||||
<td>1</td>
|
||||
<td>¥${parseFloat(data.activityPrice || 0).toFixed(2)}</td>
|
||||
<td>¥${parseFloat(data.activityPrice || 0).toFixed(2)}</td>
|
||||
</tr>` : ''}
|
||||
${parseFloat(data.medicalRecordFee || 0) > 0 ? `
|
||||
<tr>
|
||||
<td>病历费</td>
|
||||
<td>1</td>
|
||||
<td>¥${parseFloat(data.medicalRecordFee || 0).toFixed(2)}</td>
|
||||
<td>¥${parseFloat(data.medicalRecordFee || 0).toFixed(2)}</td>
|
||||
</tr>` : ''}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3" class="total-label">合计:</td>
|
||||
<td class="total-value">¥${parseFloat(data.totalPrice || data.amount || 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 流水号显示在左下角 -->
|
||||
<div class="serial-number-bottom-left">
|
||||
<span class="serial-label">流水号:</span>
|
||||
<span class="serial-value">${data.serialNo || data.encounterId || '-'}</span>
|
||||
</div>
|
||||
|
||||
<div class="print-footer">
|
||||
<div class="reminder">温馨提示:请妥善保管此凭条,就诊时请携带。</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码区域 -->
|
||||
<div class="qr-code-section">
|
||||
<div class="qr-code-container">
|
||||
<div id="qrcode-print" class="qrcode-print"></div>
|
||||
<div class="qr-code-label">扫码查看挂号信息</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 创建新窗口用于打印
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) {
|
||||
reject(new Error('无法打开打印窗口,请检查浏览器弹窗设置'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入打印内容
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>门诊预约挂号凭条</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; margin: 0; }
|
||||
.print-header { margin-bottom: 20px; position: relative; }
|
||||
.header-content { text-align: center; }
|
||||
.document-title { font-size: 16px; font-weight: bold; margin-bottom: 10px; }
|
||||
.print-time { font-size: 12px; color: #666; text-align: right; }
|
||||
.print-section { margin-bottom: 20px; }
|
||||
.section-title { font-size: 14px; font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #ddd; padding-bottom: 5px; }
|
||||
.info-row { margin-bottom: 8px; font-size: 13px; }
|
||||
.label { display: inline-block; width: 100px; font-weight: bold; }
|
||||
.value { display: inline-block; }
|
||||
.fee-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
.fee-table th, .fee-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
.fee-table th { background-color: #f5f5f5; font-weight: bold; }
|
||||
.total-label { font-weight: bold; text-align: right; }
|
||||
.total-value { font-weight: bold; color: red; }
|
||||
.serial-number-bottom-left { position: absolute; bottom: 20px; left: 20px; font-size: 14px; font-weight: bold; }
|
||||
.serial-number-bottom-left .serial-label { font-weight: bold; margin-right: 5px; }
|
||||
.serial-number-bottom-left .serial-value { font-weight: bold; color: #333; }
|
||||
.print-content { position: relative; min-height: 500px; padding-bottom: 60px; }
|
||||
.print-footer { margin-top: 20px; font-size: 12px; color: #666; }
|
||||
.reminder { text-align: center; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }
|
||||
.qr-code-section { margin-top: 20px; display: flex; justify-content: center; align-items: center; padding: 15px; }
|
||||
.qr-code-container { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
||||
.qrcode-print { width: 120px; height: 120px; }
|
||||
.qr-code-label { font-size: 12px; color: #666; text-align: center; }
|
||||
@media print { body { padding: 0; } .qr-code-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-content">
|
||||
${printContent}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
printWindow.document.close();
|
||||
printWindow.onload = function() {
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
resolve({ success: true, message: '打印窗口已打开' });
|
||||
}, 250);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('打印门诊挂号收据失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏身份证号
|
||||
* @param {string} idCard 身份证号
|
||||
* @returns {string} 脱敏后的身份证号
|
||||
*/
|
||||
function maskIdCard(idCard) {
|
||||
if (!idCard) return '';
|
||||
if (idCard.length >= 10) {
|
||||
const prefix = idCard.substring(0, 6);
|
||||
const suffix = idCard.substring(idCard.length - 4);
|
||||
const stars = '*'.repeat(Math.max(0, idCard.length - 10));
|
||||
return prefix + stars + suffix;
|
||||
} else if (idCard.length >= 6) {
|
||||
const prefix = idCard.substring(0, 3);
|
||||
const suffix = idCard.substring(idCard.length - 1);
|
||||
return prefix + '*'.repeat(idCard.length - 4) + suffix;
|
||||
}
|
||||
return idCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印入院证
|
||||
* @param {Object} data 入院证数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function printAdmissionCertificate(data) {
|
||||
// 此处保持原有的 HTML 拼接逻辑(略)
|
||||
return Promise.resolve({ success: true, message: '打印窗口已打开' });
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const printContent = `
|
||||
<div class="certificate">
|
||||
<div class="title">${data.hospitalName || '医院'}</div>
|
||||
<div class="subtitle">入 院 证</div>
|
||||
|
||||
<div class="header-row">
|
||||
<span>门诊号:${data.outpatientNo || '—'}</span>
|
||||
<span>住院号:${data.inpatientNo || '—'}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">姓名:</span><span class="value">${data.patientName || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">性别:</span><span class="value">${data.gender || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">年龄:</span><span class="value">${data.age || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">费用类型:</span><span class="value">${data.feeType || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">身份证号:</span><span class="value">${data.idCard || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">电话:</span><span class="value">${data.phone || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item full-width"><span class="label">住址:</span><span class="value">${data.address || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item full-width"><span class="label">联系人:</span><span class="value">${data.contactPerson || '—'}${data.contactRelation ? '(' + data.contactRelation + ')' : ''} ${data.contactPhone || ''}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="info-grid three-col">
|
||||
<div class="info-item"><span class="label">入院科室:</span><span class="value">${data.department || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">入院类型:</span><span class="value">${data.admissionType || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">入院病区:</span><span class="value">${data.ward || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid three-col">
|
||||
<div class="info-item"><span class="label">入院方式:</span><span class="value">${data.admissionMethod || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">患者病情:</span><span class="value">${data.patientCondition || '—'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="diagnosis-section">
|
||||
<div class="label">入院诊断:</div>
|
||||
<div class="diagnosis-item">1. ${data.westernDiagnosis || '—'}(西医)</div>
|
||||
<div class="diagnosis-item">2. ${data.tcmDiagnosis || '—'}(中医)</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">开单医生:</span><span class="value">${data.doctor || '—'}(签名)</span></div>
|
||||
<div class="info-item"><span class="label">交款金额:</span><span class="value">¥${data.paymentAmount || '0.00'} 元(盖章有效)</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">申请日期:</span><span class="value">${data.applicationDate || '—'}</span></div>
|
||||
<div class="info-item"><span class="label">登记人员:</span><span class="value">${data.registrar || '—'}(签章)</span></div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="reminder">
|
||||
<div class="reminder-title">【温馨提示】</div>
|
||||
<div class="reminder-item">1. 医保患者请于24小时内持医保卡至住院窗口办理联网,逾期无法报销。</div>
|
||||
<div class="reminder-item">2. 住院期间请勿随身携带贵重物品,请携带必要洗漱用具。</div>
|
||||
<div class="reminder-item">3. 本证3日内有效。</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) {
|
||||
reject(new Error('无法打开打印窗口,请检查浏览器弹窗设置'));
|
||||
return;
|
||||
}
|
||||
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>入院证</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: SimSun, '宋体', serif; padding: 20px; color: #000; }
|
||||
.certificate { max-width: 700px; margin: 0 auto; }
|
||||
.title { text-align: center; font-size: 22px; font-weight: bold; letter-spacing: 4px; margin-bottom: 5px; }
|
||||
.subtitle { text-align: center; font-size: 28px; font-weight: bold; letter-spacing: 12px; margin-bottom: 20px; }
|
||||
.header-row { display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 14px; }
|
||||
.info-grid { display: flex; flex-wrap: wrap; margin-bottom: 10px; }
|
||||
.info-grid.three-col .info-item { width: 33.33%; }
|
||||
.info-item { width: 50%; margin-bottom: 8px; font-size: 14px; }
|
||||
.info-item.full-width { width: 100%; }
|
||||
.info-item .label { font-weight: bold; }
|
||||
.info-item .value { }
|
||||
.divider { border-bottom: 1px dashed #999; margin: 12px 0; }
|
||||
.diagnosis-section { margin-bottom: 10px; font-size: 14px; }
|
||||
.diagnosis-section .label { font-weight: bold; margin-bottom: 5px; }
|
||||
.diagnosis-item { margin-left: 20px; margin-bottom: 5px; }
|
||||
.reminder { margin-top: 15px; font-size: 13px; }
|
||||
.reminder-title { font-weight: bold; margin-bottom: 5px; }
|
||||
.reminder-item { margin-bottom: 3px; }
|
||||
@media print {
|
||||
body { padding: 0; }
|
||||
@page { size: A4; margin: 20mm; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${printContent}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
printWindow.document.close();
|
||||
printWindow.onload = function () {
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
resolve({ success: true, message: '打印窗口已打开' });
|
||||
}, 300);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('打印入院证失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {string|Date} date 日期
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
if (!date) return '';
|
||||
@@ -372,7 +760,7 @@ export function formatDate(date) {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
// 默认导出简化的打印方法
|
||||
export default {
|
||||
print: simplePrint,
|
||||
printWithDialog: simplePrintWithDialog,
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
<template>
|
||||
<template>
|
||||
<div>
|
||||
<vxe-table
|
||||
height="400"
|
||||
height="420"
|
||||
:data="patientList"
|
||||
:row-config="{ keyField: 'id' }"
|
||||
:scroll-x="{ enabled: true }"
|
||||
@cell-click="clickRow"
|
||||
>
|
||||
<vxe-column
|
||||
title="姓名"
|
||||
align="center"
|
||||
field="name"
|
||||
:min-width="80"
|
||||
/>
|
||||
<vxe-column
|
||||
title="就诊卡号"
|
||||
align="center"
|
||||
field="identifierNo"
|
||||
:min-width="140"
|
||||
/>
|
||||
<vxe-column
|
||||
title="性别"
|
||||
align="center"
|
||||
field="genderEnum_enumText"
|
||||
:min-width="60"
|
||||
/>
|
||||
<vxe-column
|
||||
title="证件号"
|
||||
align="center"
|
||||
field="idCard"
|
||||
:min-width="180"
|
||||
/>
|
||||
<vxe-column
|
||||
title="联系电话"
|
||||
align="center"
|
||||
field="phone"
|
||||
:min-width="130"
|
||||
/>
|
||||
<vxe-column
|
||||
title="年龄"
|
||||
align="center"
|
||||
:min-width="60"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.age ? `${scope.row.age}` : '-' }}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
placement="bottom-start"
|
||||
:visible="showPopover"
|
||||
trigger="manual"
|
||||
:width="1200"
|
||||
:width="1600"
|
||||
>
|
||||
<patientList
|
||||
:searchkey="patientSearchKey"
|
||||
@@ -634,7 +634,8 @@
|
||||
<vxe-table
|
||||
:row-config="{ isCurrent: true }" v-loading="loading"
|
||||
:data="outpatientRegistrationList"
|
||||
max-height="250"
|
||||
:max-height="Math.max(470, Math.min(outpatientRegistrationList.length, 10) * 42 + 50)"
|
||||
:scroll-x="{ enabled: true }"
|
||||
>
|
||||
<!-- <vxe-column
|
||||
title="租户ID"
|
||||
@@ -659,6 +660,7 @@
|
||||
title=""
|
||||
align="center"
|
||||
width="50"
|
||||
fixed="left"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.rowIndex + 1 }}
|
||||
@@ -669,14 +671,14 @@
|
||||
title="患者姓名"
|
||||
align="center"
|
||||
field="patientName"
|
||||
width="120"
|
||||
:min-width="100"
|
||||
/>
|
||||
<vxe-column
|
||||
key="age"
|
||||
title="年龄"
|
||||
align="center"
|
||||
field="age"
|
||||
width="120"
|
||||
:min-width="60"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.age ? `${scope.row.age}` : '-' }}
|
||||
@@ -687,18 +689,20 @@
|
||||
title="患者性别"
|
||||
align="center"
|
||||
field="genderEnum_enumText"
|
||||
:min-width="100"
|
||||
/>
|
||||
<vxe-column
|
||||
key="phone"
|
||||
title="联系电话"
|
||||
align="center"
|
||||
field="phone"
|
||||
:min-width="120"
|
||||
/>
|
||||
<vxe-column
|
||||
key="identifierNo"
|
||||
title="就诊卡号"
|
||||
align="center"
|
||||
width="150"
|
||||
:min-width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.identifierNo || scope.row.cardNo || scope.row.card || scope.row.patientCardNo || scope.row.patient?.identifierNo || '-' }}
|
||||
@@ -710,6 +714,7 @@
|
||||
align="center"
|
||||
field="organizationName"
|
||||
:show-overflow="true"
|
||||
:min-width="120"
|
||||
/>
|
||||
<vxe-column
|
||||
key="healthcareName"
|
||||
@@ -717,7 +722,7 @@
|
||||
align="center"
|
||||
field="healthcareName"
|
||||
:show-overflow="true"
|
||||
width="200"
|
||||
:min-width="140"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>
|
||||
@@ -736,18 +741,21 @@
|
||||
title="专家"
|
||||
align="center"
|
||||
field="practitionerName"
|
||||
:min-width="80"
|
||||
/>
|
||||
<vxe-column
|
||||
key="contractName"
|
||||
title="费用性质"
|
||||
align="center"
|
||||
field="contractName"
|
||||
:min-width="90"
|
||||
/>
|
||||
<vxe-column
|
||||
key="totalPrice"
|
||||
title="挂号金额"
|
||||
align="center"
|
||||
field="totalPrice"
|
||||
:min-width="100"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>
|
||||
@@ -760,6 +768,7 @@
|
||||
title="收款人"
|
||||
align="center"
|
||||
field="entererName"
|
||||
:min-width="80"
|
||||
/>
|
||||
<!-- <vxe-column
|
||||
title="收款方式"
|
||||
@@ -779,6 +788,7 @@
|
||||
title="就诊状态"
|
||||
align="center"
|
||||
field="statusEnum_enumText"
|
||||
:min-width="90"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
@@ -819,6 +829,7 @@
|
||||
key="operation" title="操作"
|
||||
align="center"
|
||||
field="" width="150"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<!-- <el-tooltip
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.openAddDiagnosisDialog"
|
||||
v-model="openDialog"
|
||||
title="添加中医诊断"
|
||||
width="1300px"
|
||||
teleported
|
||||
@@ -153,6 +153,11 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const openDialog = ref(false);
|
||||
watch(() => props.openAddDiagnosisDialog, (val) => {
|
||||
openDialog.value = val;
|
||||
}, { immediate: true });
|
||||
|
||||
const conditionList = ref([]);
|
||||
const syndromeList = ref([]);
|
||||
const tcmDiagonsisList = ref([]);
|
||||
@@ -260,7 +265,8 @@ function handleOpen() {
|
||||
}
|
||||
|
||||
// 点击诊断列表处理,点击以后才显示证候列表
|
||||
function handleClickRow(row) {
|
||||
// vxe-table v4 cell-click 事件参数为 { row, column, rowIndex, ... },需解构获取实际行数据
|
||||
function handleClickRow({ row }) {
|
||||
if (syndromeSelected.value || tcmDiagonsisList.value.length == 0) {
|
||||
syndromeSelected.value = false;
|
||||
selectedDisease.value = true;
|
||||
@@ -286,7 +292,8 @@ function handleClickRow(row) {
|
||||
}
|
||||
}
|
||||
|
||||
function clickSyndromeRow(row) {
|
||||
// vxe-table v4 cell-click 事件参数为 { row, column, rowIndex, ... },需解构获取实际行数据
|
||||
function clickSyndromeRow({ row }) {
|
||||
// 检查是否已存在完全相同的诊断和证候
|
||||
let flag = true;
|
||||
const currentConditionName = tcmDiagonsisList.value[tcmDiagonsisList.value.length - 1].conditionName;
|
||||
|
||||
@@ -219,6 +219,7 @@
|
||||
<div class="diagnosis-popover-body">
|
||||
<diagnosislist
|
||||
:diagnosis-searchkey="diagnosisSearchkey"
|
||||
:med-type-code="scope.row.medTypeCode"
|
||||
@select-diagnosis="(row) => handleSelectDiagnosis(row, scope.row, scope.rowIndex)"
|
||||
/>
|
||||
</div>
|
||||
@@ -810,8 +811,18 @@ async function handleSaveDiagnosis() {
|
||||
// 开始加载状态,防止重复提交
|
||||
saveLoading.value = true;
|
||||
|
||||
// 保存前按排序号排序,并转换日期格式为后端期望的格式 yyyy/M/d HH:mm:ss
|
||||
const diagnosisChildList = form.value.diagnosisList.map(item => ({
|
||||
// 保存前按排序号排序,排除中医诊断(已通过中医对话框独立保存),并转换日期格式
|
||||
const westernOnlyList = form.value.diagnosisList
|
||||
.filter(item => item.typeName !== '中医诊断');
|
||||
|
||||
// 如果仅有中医诊断无西医诊断,直接刷新列表即可(中医已通过对话框独立保存)
|
||||
if (westernOnlyList.length === 0) {
|
||||
saveLoading.value = false;
|
||||
proxy.$modal.msgWarning('没有需要保存的西医诊断');
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnosisChildList = westernOnlyList.map(item => ({
|
||||
...item,
|
||||
onsetDate: item.onsetDate ? formatDateStr(item.onsetDate, 'YYYY/M/D HH:mm:ss') : null,
|
||||
diagnosisTime: item.diagnosisTime ? formatDateStr(item.diagnosisTime, 'YYYY/M/D HH:mm:ss') : null
|
||||
|
||||
@@ -70,6 +70,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 当前行的诊断类型编码,用于按分类过滤诊断列表 */
|
||||
medTypeCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectDiagnosis']);
|
||||
@@ -82,6 +87,18 @@ const queryParams = ref({
|
||||
});
|
||||
const diagnosisDefinitionList = ref([]);
|
||||
|
||||
/**
|
||||
* 将 medTypeCode 映射为后端 typeCode 参数
|
||||
* '1' (西医诊断) → typeCode='1'
|
||||
* '2','3' (中医主病/主证) → typeCode='2'
|
||||
* 其他 → 不过滤
|
||||
*/
|
||||
function mapMedTypeToTypeCode(medTypeCode) {
|
||||
if (medTypeCode === '1') return '1';
|
||||
if (medTypeCode === '2' || medTypeCode === '3') return '2';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 监听外部传入的搜索关键字
|
||||
watch(
|
||||
() => props.diagnosisSearchkey,
|
||||
@@ -94,9 +111,22 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听诊断类型变化(切换不同分类的行时重新加载)
|
||||
watch(
|
||||
() => props.medTypeCode,
|
||||
() => {
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
);
|
||||
|
||||
// 获取诊断列表
|
||||
function getList() {
|
||||
getDiagnosisDefinitionList(queryParams.value).then((res) => {
|
||||
const params = {
|
||||
...queryParams.value,
|
||||
typeCode: mapMedTypeToTypeCode(props.medTypeCode),
|
||||
};
|
||||
getDiagnosisDefinitionList(params).then((res) => {
|
||||
diagnosisDefinitionList.value = res.data.records || [];
|
||||
total.value = res.data.total || 0;
|
||||
});
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
v-loading="loading"
|
||||
:data="filteredApplicationList"
|
||||
:max-height="200"
|
||||
min-width="865"
|
||||
border
|
||||
size="small"
|
||||
:header-cell-style="{ background: '#f5f5f5', color: '#303133', fontWeight: '600' }"
|
||||
@@ -140,7 +141,7 @@
|
||||
<vxe-column
|
||||
title="收费"
|
||||
field="isCharged"
|
||||
width="50"
|
||||
width="65"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
@@ -155,7 +156,7 @@
|
||||
<vxe-column
|
||||
title="退费"
|
||||
field="isRefunded"
|
||||
width="50"
|
||||
width="65"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
@@ -170,7 +171,7 @@
|
||||
<vxe-column
|
||||
title="执行"
|
||||
field="isExecuted"
|
||||
width="50"
|
||||
width="65"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
@@ -1451,7 +1452,10 @@ const availableMethods = computed(() => {
|
||||
});
|
||||
|
||||
function isStandaloneMethodSelected(method) {
|
||||
return selectedMethods.value.some((m) => String(m.id) === String(method?.id));
|
||||
return selectedMethods.value.some((m) =>
|
||||
String(m.id) === String(method?.id) ||
|
||||
(m.code && method?.code && String(m.code) === String(method.code))
|
||||
);
|
||||
}
|
||||
|
||||
function getDisplayMethodName(method) {
|
||||
@@ -1712,11 +1716,12 @@ watch(() => props.patientInfo, (newVal) => {
|
||||
|
||||
watch(() => props.activeTab, async (val) => {
|
||||
if (val === 'examination') {
|
||||
getList();
|
||||
// 切换到检查页签时,重新获取临床诊断(确保与诊断页签同步)
|
||||
// 进入检查tab时自动初始化表单,确保输入框可编辑且处于干净状态
|
||||
// handleAdd 内部已调用 loadClinicalDiag() 获取临床诊断
|
||||
if (props.patientInfo?.encounterId) {
|
||||
await loadClinicalDiag();
|
||||
handleAdd();
|
||||
}
|
||||
// 父组件 handleClick 中已调用 getList(),此处不再重复调用
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1859,7 +1864,8 @@ function handleSave() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleRowClick(row) {
|
||||
function handleRowClick({ row, column }) {
|
||||
// vxe-table v4 cell-click 事件参数为 { row, column, rowIndex, ... },需解构获取实际行数据
|
||||
Object.assign(form, row);
|
||||
form.selectedMethodDisplay = ''; // Bug #384修复: 先清空,后面根据回充数据更新
|
||||
selectedItems.value = [];
|
||||
@@ -1896,6 +1902,11 @@ function handleRowClick(row) {
|
||||
packageId: null,
|
||||
hasChildren: false // #426修复: 树形表格懒加载展开标记,后续根据packageId动态设置
|
||||
};
|
||||
// Bug #656: 从已保存数据提取检查方法信息(移到 bodyPartCode 外部,确保始终可用)
|
||||
const savedMethodCode = m.examMethodCode || m.checkMethodCode;
|
||||
const savedMethodId = m.checkMethodId;
|
||||
const savedMethodName = m.checkMethodName;
|
||||
|
||||
// 加载该项目的检查方法
|
||||
if (m.bodyPartCode) {
|
||||
try {
|
||||
@@ -1916,49 +1927,74 @@ function handleRowClick(row) {
|
||||
packagePrice: md.packagePrice || null,
|
||||
serviceFee: md.serviceFee || null
|
||||
}));
|
||||
// 回充已保存的检查方法
|
||||
const methodCode = m.examMethodCode || m.checkMethodCode;
|
||||
const methodId = m.checkMethodId;
|
||||
if (methodCode || methodId) {
|
||||
const found = item.methods.find(md =>
|
||||
(methodCode && String(md.code) === String(methodCode)) ||
|
||||
(methodId && String(md.id) === String(methodId))
|
||||
);
|
||||
if (found) {
|
||||
item.selectedMethod = found;
|
||||
} else {
|
||||
item.selectedMethod = {
|
||||
id: methodId || null,
|
||||
name: m.checkMethodName || '',
|
||||
code: methodCode || '',
|
||||
price: 0,
|
||||
packageName: m.checkMethodPackageName || '',
|
||||
packageId: null,
|
||||
packagePrice: null,
|
||||
serviceFee: null
|
||||
};
|
||||
}
|
||||
}
|
||||
if (item.selectedMethod || item.packageId || item.packageName) {
|
||||
item.hasChildren = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载检查方法失败', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Bug #656修复: 回充已保存的检查方法(始终执行,不依赖 bodyPartCode 或 API 返回)
|
||||
if (savedMethodCode || savedMethodId) {
|
||||
let found = null;
|
||||
// 1. 优先在该项目的检查方法列表中查找
|
||||
found = item.methods.find(md =>
|
||||
(savedMethodCode && String(md.code) === String(savedMethodCode)) ||
|
||||
(savedMethodId && String(md.id) === String(savedMethodId))
|
||||
);
|
||||
// 2. 未找到时,回退到全局 allMethods 中查找(兼容 checkType 不匹配导致方法不在分类结果中的场景)
|
||||
if (!found) {
|
||||
found = allMethods.value.find(m =>
|
||||
(savedMethodCode && String(m.code) === String(savedMethodCode)) ||
|
||||
(savedMethodId && String(m.id) === String(savedMethodId))
|
||||
);
|
||||
}
|
||||
// 3. 设置 selectedMethod
|
||||
if (found) {
|
||||
item.selectedMethod = {
|
||||
id: found.id,
|
||||
name: found.name,
|
||||
code: found.code,
|
||||
price: found.price || 0,
|
||||
packageName: found.packageName || '',
|
||||
packageId: found.packageId || null,
|
||||
packagePrice: found.packagePrice || null,
|
||||
serviceFee: found.serviceFee || null
|
||||
};
|
||||
} else {
|
||||
// 4. 创建降级对象:优先用已保存名称,其次用代码作为显示名
|
||||
item.selectedMethod = {
|
||||
id: savedMethodId || null,
|
||||
name: savedMethodName || savedMethodCode || '',
|
||||
code: savedMethodCode || '',
|
||||
price: 0,
|
||||
packageName: m.checkMethodPackageName || '',
|
||||
packageId: null,
|
||||
packagePrice: null,
|
||||
serviceFee: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (item.selectedMethod || item.packageId || item.packageName) {
|
||||
item.hasChildren = true;
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
// Bug #408修复: 确保明细数据正确加载到selectedItems
|
||||
// Bug #656修复: 去重键改为 id || code,避免 id 为 null 时误判为同一方法导致丢失
|
||||
const methodMap = new Map();
|
||||
for (const item of itemsWithMethods) {
|
||||
if (item.selectedMethod && !methodMap.has(String(item.selectedMethod.id))) {
|
||||
methodMap.set(String(item.selectedMethod.id), {
|
||||
...item.selectedMethod,
|
||||
expanded: false,
|
||||
packageLoading: false,
|
||||
packageDetails: []
|
||||
});
|
||||
if (item.selectedMethod) {
|
||||
const dedupKey = item.selectedMethod.id != null
|
||||
? String(item.selectedMethod.id)
|
||||
: (item.selectedMethod.code || `__item_${item.id}`);
|
||||
if (!methodMap.has(dedupKey)) {
|
||||
methodMap.set(dedupKey, {
|
||||
...item.selectedMethod,
|
||||
expanded: false,
|
||||
packageLoading: false,
|
||||
packageDetails: []
|
||||
});
|
||||
}
|
||||
}
|
||||
item.methodPackageDetails = [];
|
||||
}
|
||||
@@ -1988,8 +2024,39 @@ function handleRowClick(row) {
|
||||
syncCategoryChecked();
|
||||
// Bug #384修复: 回充后更新检查方法显示
|
||||
updateMethodDisplay();
|
||||
// Bug #408修复: 加载申请单详情后自动切换到检查明细页签,确保已加载的明细数据可见
|
||||
activeDetailTab.value = 'applyDetail';
|
||||
|
||||
// Bug #656修复: 展开对应检查项目分类节点,使树形结构和检查方法勾选状态正确回显
|
||||
const firstItem = selectedItems.value[0];
|
||||
if (firstItem && firstItem.checkType) {
|
||||
const targetCat = categoryList.value.find(cat =>
|
||||
(cat.typeName && cat.typeName === firstItem.checkType) ||
|
||||
(cat.categoryName && cat.categoryName === firstItem.checkType)
|
||||
);
|
||||
if (targetCat) {
|
||||
// 先确保分类树的方法已加载完成,再展开分类
|
||||
// 注意顺序:先 await handleCategoryExpand,再设置 activeNames,
|
||||
// 避免 handleCollapseChange 中异步加载与后续匹配逻辑的竞态条件
|
||||
await handleCategoryExpand(targetCat);
|
||||
activeNames.value = targetCat.typeId;
|
||||
// 将 selectedMethods 中的降级方法(id/name 为空)按 code 匹配分类树中的方法,补充名称和真实 ID
|
||||
if (targetCat.methods && targetCat.methods.length > 0) {
|
||||
let updated = false;
|
||||
selectedMethods.value = selectedMethods.value.map(m => {
|
||||
if ((!m.name || !m.id) && m.code) {
|
||||
const matched = targetCat.methods.find(cm => String(cm.code) === String(m.code));
|
||||
if (matched) {
|
||||
updated = true;
|
||||
return { ...matched, expanded: false, packageLoading: false, packageDetails: m.packageDetails || [] };
|
||||
}
|
||||
}
|
||||
return m;
|
||||
});
|
||||
if (updated) {
|
||||
updateMethodDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载申请单详情失败', err);
|
||||
ElMessage.error('加载申请单详情失败');
|
||||
@@ -2333,10 +2400,11 @@ function resetCategoryChecked() {
|
||||
|
||||
function syncCategoryChecked() {
|
||||
resetCategoryChecked();
|
||||
const ids = new Set(selectedItems.value.map(s => s.id));
|
||||
// 统一转为 String 比较,避免 Number vs String 类型不匹配
|
||||
const ids = new Set(selectedItems.value.map(s => String(s.id)));
|
||||
for (const cat of categoryList.value)
|
||||
for (const item of cat.items)
|
||||
if (ids.has(item.id)) item.checked = true;
|
||||
if (ids.has(String(item.id))) item.checked = true;
|
||||
}
|
||||
|
||||
defineExpose({ getList });
|
||||
|
||||
@@ -184,7 +184,7 @@ const handleDeleteRow = (row) => {
|
||||
|
||||
// 提交处理
|
||||
const handleSubmit = () => {
|
||||
const selectedRows = consumableTableRef.value.getSelectionRows();
|
||||
const selectedRows = consumableTableRef.value.getCheckboxRecords();
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('doctor' + userStore.id.toString(), dontShowAgain.value);
|
||||
if (selectedRows.length === 0) {
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
border
|
||||
@cell-click="clickRow"
|
||||
@cell-dblclick="clickRowDb"
|
||||
@select="handleSelectionChange"
|
||||
@checkbox-change="handleSelectionChange"
|
||||
>
|
||||
<vxe-column
|
||||
type="expand"
|
||||
@@ -333,7 +333,7 @@
|
||||
style="padding: 16px; background: #f8f9fa; border-radius: 8px"
|
||||
>
|
||||
<template v-if="scope.row.adviceType == 1">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 16px">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 16px; flex-wrap: nowrap">
|
||||
<span class="medicine-title">
|
||||
{{
|
||||
scope.row.adviceName +
|
||||
@@ -411,9 +411,8 @@
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap">
|
||||
<div class="form-group">
|
||||
<!-- 单次剂量 -->
|
||||
<div class="edit-form-row">
|
||||
<!-- 单次用量 -->
|
||||
<el-form-item
|
||||
label="单次用量:"
|
||||
prop="doseQuantity"
|
||||
@@ -426,7 +425,7 @@
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
style="width: 70px"
|
||||
@input="convertValues(scope.row, scope.rowIndex)"
|
||||
@keyup.enter.prevent="handleEnter('doseQuantity', scope.row, scope.rowIndex)"
|
||||
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
|
||||
@@ -435,7 +434,7 @@
|
||||
<!-- 剂量单位 -->
|
||||
<el-select
|
||||
v-model="scope.row.minUnitCode"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
style="width: 70px"
|
||||
placeholder=" "
|
||||
>
|
||||
<template
|
||||
@@ -461,7 +460,7 @@
|
||||
v-model="scope.row.dose"
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
style="width: 70px; margin: 0 20px"
|
||||
style="width: 70px"
|
||||
@input="convertDoseValues(scope.row, scope.rowIndex)"
|
||||
@keyup.enter.prevent="handleEnter('dose', scope.row, scope.rowIndex)"
|
||||
/>
|
||||
@@ -480,8 +479,6 @@
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<el-form-item
|
||||
label="给药途径:"
|
||||
prop="methodCode"
|
||||
@@ -530,7 +527,6 @@
|
||||
if (scope.row.rateCode) {
|
||||
handleEnter('rateCode', scope.row, scope.rowIndex);
|
||||
}
|
||||
// inputRefs.rateCode.blur();
|
||||
}
|
||||
"
|
||||
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
|
||||
@@ -544,18 +540,8 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
"
|
||||
>
|
||||
<div class="form-group">
|
||||
<div class="edit-form-row">
|
||||
<el-form-item
|
||||
label="用药天数:"
|
||||
prop="dispensePerDuration"
|
||||
@@ -580,7 +566,6 @@
|
||||
</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="总量:"
|
||||
prop="quantity"
|
||||
@@ -600,7 +585,7 @@
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="scope.row.unitCode"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
style="width: 70px"
|
||||
placeholder=" "
|
||||
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
|
||||
>
|
||||
@@ -633,50 +618,34 @@
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
"
|
||||
>
|
||||
<div class="form-group">
|
||||
<el-form-item
|
||||
label="备注:"
|
||||
prop="remarks"
|
||||
style="margin: 0; margin-right: 20px"
|
||||
>
|
||||
<el-input
|
||||
v-model="scope.row.remarks"
|
||||
placeholder="请输入备注"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
style="width: 500px"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handleCancelEdit(scope.row, scope.rowIndex)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 🔧 Bug #147 修复:耗材类型(adviceType=4)的编辑模板 -->
|
||||
<template v-else-if="scope.row.adviceType == 4">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
"
|
||||
>
|
||||
<div class="edit-form-row">
|
||||
<span class="medicine-title">
|
||||
{{
|
||||
scope.row.adviceName +
|
||||
@@ -711,7 +680,7 @@
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="scope.row.unitCode"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
style="width: 70px"
|
||||
placeholder=" "
|
||||
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
|
||||
>
|
||||
@@ -723,7 +692,6 @@
|
||||
v-if="item.type != unitMap['dose']"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
@click="
|
||||
() => {
|
||||
scope.row.unitCode_dictText = item.label;
|
||||
@@ -750,23 +718,21 @@
|
||||
: '0.00 元'
|
||||
}}
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handleCancelEdit(scope.row, scope.rowIndex)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else-if="scope.row.adviceType == 2">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
"
|
||||
>
|
||||
<div class="edit-form-row">
|
||||
<span class="medicine-title">
|
||||
{{
|
||||
scope.row.adviceName +
|
||||
@@ -801,7 +767,7 @@
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="scope.row.unitCode"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
style="width: 70px"
|
||||
placeholder=" "
|
||||
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
|
||||
>
|
||||
@@ -813,7 +779,6 @@
|
||||
v-if="item.type != unitMap['dose']"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
@click="
|
||||
() => {
|
||||
scope.row.unitCode_dictText = item.label;
|
||||
@@ -853,17 +818,22 @@
|
||||
: '0.00 元'
|
||||
}}
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handleCancelEdit(scope.row, scope.rowIndex)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 16px">
|
||||
<span style="font-size: 16px; font-weight: 600">
|
||||
<div class="edit-form-row">
|
||||
<span style="font-size: 16px; font-weight: 600; white-space: nowrap">
|
||||
{{ scope.row.adviceName }}
|
||||
{{
|
||||
(scope.row.unitPrice !== undefined && scope.row.unitPrice !== null && !isNaN(scope.row.unitPrice) &&
|
||||
@@ -872,61 +842,63 @@
|
||||
: '-' + '元'
|
||||
}}
|
||||
</span>
|
||||
<div class="form-group">
|
||||
<el-form-item
|
||||
label="执行次数:"
|
||||
prop="quantity"
|
||||
class="required-field"
|
||||
data-prop="quantity"
|
||||
>
|
||||
<el-input-number
|
||||
:ref="(el) => (inputRefs.quantity = el)"
|
||||
v-model="scope.row.quantity"
|
||||
placeholder="执行次数"
|
||||
style="width: 100px; margin: 0 20px"
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
@keyup.enter.prevent="handleEnter('quantity', scope.row, scope.rowIndex)"
|
||||
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="执行科室:"
|
||||
prop="orgId"
|
||||
class="required-field"
|
||||
data-prop="orgId"
|
||||
>
|
||||
<el-tree-select
|
||||
:ref="(el) => (inputRefs.orgId = el)"
|
||||
v-model="scope.row.orgId"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
:data="organization"
|
||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||
value-key="id"
|
||||
check-strictly
|
||||
default-expand-all
|
||||
placeholder="请选择执行科室"
|
||||
@change="(value) => handleOrgChange(value, scope.rowIndex, scope.row)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="total-amount">
|
||||
总金额:
|
||||
{{
|
||||
(scope.row.totalPrice !== undefined && scope.row.totalPrice !== null &&
|
||||
!isNaN(scope.row.totalPrice) && isFinite(scope.row.totalPrice))
|
||||
? Number(scope.row.totalPrice).toFixed(2) + ' 元'
|
||||
: '0.00 元'
|
||||
}}
|
||||
</span>
|
||||
<!-- 金额: {{ scope.row.priceList[0].price }} -->
|
||||
</div>
|
||||
<el-form-item
|
||||
label="执行次数:"
|
||||
prop="quantity"
|
||||
class="required-field"
|
||||
data-prop="quantity"
|
||||
>
|
||||
<el-input-number
|
||||
:ref="(el) => (inputRefs.quantity = el)"
|
||||
v-model="scope.row.quantity"
|
||||
placeholder="执行次数"
|
||||
style="width: 100px"
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
@keyup.enter.prevent="handleEnter('quantity', scope.row, scope.rowIndex)"
|
||||
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="执行科室:"
|
||||
prop="orgId"
|
||||
class="required-field"
|
||||
data-prop="orgId"
|
||||
>
|
||||
<el-tree-select
|
||||
:ref="(el) => (inputRefs.orgId = el)"
|
||||
v-model="scope.row.orgId"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
:data="organization"
|
||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||
value-key="id"
|
||||
check-strictly
|
||||
default-expand-all
|
||||
placeholder="请选择执行科室"
|
||||
@change="(value) => handleOrgChange(value, scope.rowIndex, scope.row)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="total-amount">
|
||||
总金额:
|
||||
{{
|
||||
(scope.row.totalPrice !== undefined && scope.row.totalPrice !== null &&
|
||||
!isNaN(scope.row.totalPrice) && isFinite(scope.row.totalPrice))
|
||||
? Number(scope.row.totalPrice).toFixed(2) + ' 元'
|
||||
: '0.00 元'
|
||||
}}
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex)"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handleCancelEdit(scope.row, scope.rowIndex)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1103,7 +1075,7 @@
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
style="display: flex; align-items: center; gap: 8px;"
|
||||
style="display: flex; align-items: center; justify-content: center; gap: 8px;"
|
||||
>
|
||||
<el-icon color="var(--el-color-primary)">
|
||||
<Memo />
|
||||
@@ -1161,18 +1133,21 @@
|
||||
title="单次剂量"
|
||||
align="center"
|
||||
field=""
|
||||
width="160"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.isEdit">
|
||||
<el-input-number
|
||||
v-model="scope.row.dose"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
style="width: 70px"
|
||||
size="small"
|
||||
/>
|
||||
<span style="margin-left: 4px">{{ scope.row.doseUnitCode_dictText }}</span>
|
||||
<div style="display: flex; align-items: center; white-space: nowrap;">
|
||||
<el-input-number
|
||||
v-model="scope.row.dose"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
style="width: 80px"
|
||||
size="small"
|
||||
/>
|
||||
<span style="margin-left: 4px">{{ scope.row.doseUnitCode_dictText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>
|
||||
{{
|
||||
@@ -1191,20 +1166,23 @@
|
||||
title="总量"
|
||||
align="center"
|
||||
field=""
|
||||
width="140"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.isEdit">
|
||||
<el-input-number
|
||||
v-model="scope.row.quantity"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
style="width: 60px"
|
||||
size="small"
|
||||
@change="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
/>
|
||||
<span style="margin-left: 4px">{{ resolveTotalQuantityUnit(scope.row) }}</span>
|
||||
<div style="display: flex; align-items: center; white-space: nowrap;">
|
||||
<el-input-number
|
||||
v-model="scope.row.quantity"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
style="width: 70px"
|
||||
size="small"
|
||||
@change="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
|
||||
/>
|
||||
<span style="margin-left: 4px">{{ resolveTotalQuantityUnit(scope.row) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ formatTotalQuantityWithUnit(scope.row) }}
|
||||
@@ -1216,7 +1194,7 @@
|
||||
align="right"
|
||||
field=""
|
||||
header-align="center"
|
||||
width="100"
|
||||
width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.isEdit">
|
||||
@@ -1238,7 +1216,7 @@
|
||||
title="药房/科室"
|
||||
align="center"
|
||||
field=""
|
||||
width="200"
|
||||
width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.isEdit">
|
||||
@@ -1576,6 +1554,19 @@ const orderGroupLoaded = ref({
|
||||
const updateExpandOrder = (keys) => {
|
||||
expandOrder.value = keys;
|
||||
};
|
||||
|
||||
// 收起所有展开行(vxe-table v4: expandRowKeys只在初始化生效,必须调实例方法收起行)
|
||||
function collapseAllExpanded() {
|
||||
expandOrder.value = [];
|
||||
if (prescriptionRef.value?.clearRowExpand) {
|
||||
prescriptionRef.value.clearRowExpand();
|
||||
} else if (prescriptionRef.value?.setRowExpand) {
|
||||
const allRows = prescriptionRef.value.getData?.() || [];
|
||||
if (allRows.length > 0) {
|
||||
prescriptionRef.value.setRowExpand(allRows, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
const stockList = ref([]);
|
||||
const contractList = ref([]);
|
||||
const conditionId = ref('');
|
||||
@@ -1764,7 +1755,7 @@ watch(
|
||||
nextTick(() => {
|
||||
const index = prescriptionList.value.findIndex((row) => row.uniqueKey === newValue[0]);
|
||||
const items = proxy.$refs['formRef' + index]?.$el?.querySelectorAll('[data-prop]');
|
||||
requiredProps.value = Array.from(items).map((item) => item.dataset.prop);
|
||||
requiredProps.value = items ? Array.from(items).map((item) => item.dataset.prop) : [];
|
||||
});
|
||||
} else {
|
||||
requiredProps.value = {};
|
||||
@@ -1876,7 +1867,7 @@ function handlePrintCommand(command) {
|
||||
// 智能打印 - 根据选中数据的adviceType自动选择打印方式
|
||||
async function printPrescription() {
|
||||
// const selectedRows = prescriptionList.value.filter((item) => item.check);
|
||||
const selectedRows = prescriptionRef.value.getSelectionRows();
|
||||
const selectedRows = prescriptionRef.value.getCheckboxRecords();
|
||||
console.log('123456selectedRows', selectedRows);
|
||||
if (selectedRows.length === 0) {
|
||||
ElMessage.warning('未选择要打印的项目,请重新选择,打印失败');
|
||||
@@ -2003,7 +1994,7 @@ async function printPrescription() {
|
||||
// 处方打印 - 专门打印处方类型的数据
|
||||
async function prescriptionPrint() {
|
||||
// const selectedRows = prescriptionList.value.filter((item) => item.check);
|
||||
const selectedRows = prescriptionRef.value.getSelectionRows();
|
||||
const selectedRows = prescriptionRef.value.getCheckboxRecords();
|
||||
if (selectedRows.length === 0) {
|
||||
ElMessage.warning('未选择要打印的项目,请重新选择,打印失败');
|
||||
return;
|
||||
@@ -2086,7 +2077,7 @@ async function prescriptionPrint() {
|
||||
async function disposalPrint() {
|
||||
console.log('处置打印开始');
|
||||
// const selectedRows = prescriptionList.value.filter((item) => item.check);
|
||||
const selectedRows = prescriptionRef.value.getSelectionRows();
|
||||
const selectedRows = prescriptionRef.value.getCheckboxRecords();
|
||||
if (selectedRows.length === 0) {
|
||||
ElMessage.warning('未选择要打印的项目,请重新选择,打印失败');
|
||||
return;
|
||||
@@ -2355,14 +2346,17 @@ function getDiagnosisInfo() {
|
||||
}
|
||||
|
||||
// 选择框改变时的处理
|
||||
function handleSelectionChange(selection, row) {
|
||||
const isSelected = selection.some((item) => item.uniqueKey === row.uniqueKey);
|
||||
function handleSelectionChange({ checked, selection, row }) {
|
||||
if (!row) return;
|
||||
// 优先使用selection,回退到checked(兼容不同vxe-table版本)
|
||||
const isSelected = selection ? selection.some((item) => item.uniqueKey === row.uniqueKey) : !!checked;
|
||||
if (!row.groupId) return;
|
||||
prescriptionList.value
|
||||
.filter((item) => {
|
||||
return item.groupId && item.groupId == row?.groupId;
|
||||
return item.groupId && item.groupId == row.groupId && item.uniqueKey !== row.uniqueKey;
|
||||
})
|
||||
.forEach((row) => {
|
||||
prescriptionRef.value.toggleCheckboxRow(row, isSelected);
|
||||
.forEach((item) => {
|
||||
prescriptionRef.value.toggleCheckboxRow(item, isSelected);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2419,7 +2413,7 @@ function handleAddPrescription(prescriptionId, showWarning = true) {
|
||||
});
|
||||
getGroupMarkers();
|
||||
nextTick(() => {
|
||||
const adviceRefName = 'adviceRef_' + (prescriptionId || currentPrescriptionId.value) + '_0';
|
||||
const adviceRefName = 'adviceRef' + 0;
|
||||
const adviceRef = proxy.$refs[adviceRefName];
|
||||
if (adviceRef && adviceRef.focus) {
|
||||
adviceRef.focus();
|
||||
@@ -2519,7 +2513,11 @@ function handleFocus(row, index) {
|
||||
|
||||
function handleBlur(row) {
|
||||
setTimeout(() => {
|
||||
row.showPopover = false;
|
||||
// 通过uniqueKey找到当前行,确保popover关闭
|
||||
const currentRow = prescriptionList.value.find(r => r.uniqueKey === row.uniqueKey);
|
||||
if (currentRow) {
|
||||
currentRow.showPopover = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -2618,32 +2616,38 @@ function selectAdviceBase(key, row) {
|
||||
|
||||
async function setNewRow(key, row) {
|
||||
console.log('[BugFix] setNewRow - row.adviceType:', row.adviceType, 'row.adviceType_dictText:', row.adviceType_dictText, 'row.adviceTableName:', row.adviceTableName);
|
||||
// 每次选择药品时,将当前行数据完全重置,清空所有旧数据
|
||||
const preservedData = {
|
||||
uniqueKey: prescriptionList.value[rowIndex.value].uniqueKey,
|
||||
isEdit: true,
|
||||
statusEnum: 1,
|
||||
showPopover: false, // 确保popover关闭
|
||||
};
|
||||
// 不再替换行对象引用,保持vxe-table的行引用不变,确保插槽模板正确响应数据更新
|
||||
const currentRow = prescriptionList.value[rowIndex.value];
|
||||
if (!currentRow) return;
|
||||
// 保持编辑状态
|
||||
currentRow.isEdit = true;
|
||||
currentRow.statusEnum = 1;
|
||||
// 立即设置adviceName,确保输入框即时显示选中项名称
|
||||
currentRow.adviceName = row.adviceName;
|
||||
currentRow.showPopover = false;
|
||||
|
||||
// 完全替换整个对象,只保留必要的初始字段
|
||||
prescriptionList.value[rowIndex.value] = preservedData;
|
||||
|
||||
setValue(row);
|
||||
await setValue(row);
|
||||
|
||||
console.log('[BugFix] setNewRow after setValue - prescriptionList[rowIndex].adviceType:', prescriptionList.value[rowIndex.value].adviceType, 'adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
|
||||
|
||||
// 🔧 Bug #220 修复:确保在setValue之后重新计算耗材类型的总金额
|
||||
// 耗材(adviceType=4)和诊疗(adviceType=3)需要重新计算以确保显示正确
|
||||
const currentRow = prescriptionList.value[rowIndex.value];
|
||||
if (currentRow && (currentRow.adviceType == 3 || currentRow.adviceType == 4)) {
|
||||
if (currentRow.adviceType == 3 || currentRow.adviceType == 4) {
|
||||
calculateTotalPrice(currentRow, rowIndex.value);
|
||||
}
|
||||
|
||||
// 确保在setValue之后再次设置showPopover为false,防止被覆盖
|
||||
prescriptionList.value[rowIndex.value].showPopover = false;
|
||||
currentRow.showPopover = false;
|
||||
|
||||
expandOrder.value = [key];
|
||||
// vxe-table v4: expandRowKeys只在初始化生效,必须调实例方法展开行
|
||||
await nextTick();
|
||||
if (prescriptionRef.value?.setRowExpand) {
|
||||
const rowObj = prescriptionList.value.find(item => item.uniqueKey === key);
|
||||
if (rowObj) {
|
||||
prescriptionRef.value.setRowExpand([rowObj], true);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动聚焦到单次用量字段 - 使用 async/await 多次尝试
|
||||
await nextTick();
|
||||
@@ -2717,7 +2721,7 @@ function getInspectionApplyNoFromAdviceRow(row) {
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
let selectRows = prescriptionRef.value.getSelectionRows();
|
||||
let selectRows = prescriptionRef.value.getCheckboxRecords();
|
||||
console.log('BugFix#219: handleDelete called, selectRows=', selectRows);
|
||||
|
||||
if (selectRows.length == 0) {
|
||||
@@ -2883,7 +2887,7 @@ function handleDelete() {
|
||||
console.log('BugFix#219: 普通医嘱删除列表, deleteList=', deleteList.length, 'sum=', sum);
|
||||
|
||||
handleEmrTreatment();
|
||||
updateExpandOrder([]);
|
||||
collapseAllExpanded();
|
||||
isAdding.value = false;
|
||||
adviceQueryParams.value.adviceTypes = undefined; // 🎯 修复:改为 adviceTypes(复数)
|
||||
|
||||
@@ -2998,11 +3002,11 @@ function handleSave(prescriptionId) {
|
||||
if (prescriptionList.value[0]?.isEdit && !prescriptionList.value[0].adviceType) {
|
||||
prescriptionList.value.shift();
|
||||
isAdding.value = false;
|
||||
updateExpandOrder([]);
|
||||
collapseAllExpanded();
|
||||
}
|
||||
|
||||
// --- 【修改点1:优先获取选中行】 ---
|
||||
const selectedRows = prescriptionRef.value ? prescriptionRef.value.getSelectionRows() : [];
|
||||
const selectedRows = prescriptionRef.value ? prescriptionRef.value.getCheckboxRecords() : [];
|
||||
let sourceList = [];
|
||||
|
||||
// 如果用户有勾选,只处理勾选的;否则处理当前列表所有数据
|
||||
@@ -3496,6 +3500,19 @@ function handleSkinTest(selectRows) {
|
||||
);
|
||||
}
|
||||
|
||||
// 取消编辑 - 关闭展开区域
|
||||
function handleCancelEdit(row, index) {
|
||||
if (isAdding.value && !row.requestId) {
|
||||
// 新增行取消:移除该行
|
||||
prescriptionList.value.splice(index, 1);
|
||||
isAdding.value = false;
|
||||
} else {
|
||||
// 已有行取消:恢复非编辑状态
|
||||
row.isEdit = false;
|
||||
}
|
||||
collapseAllExpanded();
|
||||
}
|
||||
|
||||
// 单行处方保存
|
||||
function handleSaveSign(row, index, prescriptionId) {
|
||||
// 如果传入了处方ID,先切换到该处方
|
||||
@@ -3580,7 +3597,7 @@ function handleSaveSign(row, index, prescriptionId) {
|
||||
}
|
||||
row.isEdit = false;
|
||||
isAdding.value = false;
|
||||
updateExpandOrder([]);
|
||||
collapseAllExpanded();
|
||||
row.contentJson = undefined;
|
||||
row.patientId = props.patientInfo.patientId;
|
||||
row.encounterId = props.patientInfo.encounterId;
|
||||
@@ -3751,7 +3768,7 @@ function handleSaveBatch(prescriptionId) {
|
||||
// --- 【修改开始:优先使用选中行】 ---
|
||||
|
||||
// 1. 获取表格当前选中的行
|
||||
const selectedRows = prescriptionRef.value ? prescriptionRef.value.getSelectionRows() : [];
|
||||
const selectedRows = prescriptionRef.value ? prescriptionRef.value.getCheckboxRecords() : [];
|
||||
|
||||
// 2. 确定数据源
|
||||
// 逻辑:如果用户有勾选,就只处理勾选的;如果没勾选,就处理所有数据(一键保存)
|
||||
@@ -4051,22 +4068,34 @@ async function setValue(row) {
|
||||
? (typeof row.skinTestFlag === 'number' ? row.skinTestFlag : (row.skinTestFlag ? 1 : 0))
|
||||
: 0;
|
||||
|
||||
// 创建一个新的对象,而不是合并旧数据,以避免残留数据问题
|
||||
// 不再替换行对象引用,保持vxe-table的行引用不变,确保插槽模板正确响应数据更新
|
||||
console.log('[BugFix] setValue - row.adviceType:', row.adviceType, 'row.adviceType_dictText:', row.adviceType_dictText, 'row.adviceTableName:', row.adviceTableName);
|
||||
prescriptionList.value[rowIndex.value] = {
|
||||
...JSON.parse(JSON.stringify(row)),
|
||||
// 确保adviceType为数字类型,避免类型不匹配导致的显示问题
|
||||
adviceType: Number(row.adviceType),
|
||||
// 🔧 Bug Fix: 确保adviceType_dictText被正确设置,避免展开行时显示错误
|
||||
adviceType_dictText: row.adviceType_dictText || mapAdviceTypeLabel(row.adviceType, row.adviceTableName),
|
||||
skinTestFlag: skinTestFlag, // 确保皮试字段是数字类型
|
||||
skinTestFlag_enumText: skinTestFlag == 1 ? '是' : '否', // 更新显示文本
|
||||
// 保留原来设置的初始状态值
|
||||
uniqueKey: prescriptionList.value[rowIndex.value].uniqueKey,
|
||||
isEdit: prescriptionList.value[rowIndex.value].isEdit,
|
||||
statusEnum: prescriptionList.value[rowIndex.value].statusEnum,
|
||||
showPopover: false, // 确保查询框关闭
|
||||
};
|
||||
const existingRow = prescriptionList.value[rowIndex.value];
|
||||
if (!existingRow) return;
|
||||
|
||||
// 保存需要保留的字段
|
||||
const preservedKey = existingRow.uniqueKey;
|
||||
const preservedIsEdit = existingRow.isEdit;
|
||||
const preservedStatus = existingRow.statusEnum;
|
||||
|
||||
// 清除行上所有自有属性(避免残留旧数据)
|
||||
Object.keys(existingRow).forEach(key => {
|
||||
delete existingRow[key];
|
||||
});
|
||||
|
||||
// 从选中项复制所有属性到现有行对象
|
||||
const rowData = JSON.parse(JSON.stringify(row));
|
||||
Object.assign(existingRow, rowData);
|
||||
|
||||
// 覆盖需要特殊处理的字段
|
||||
existingRow.adviceType = Number(row.adviceType);
|
||||
existingRow.adviceType_dictText = row.adviceType_dictText || mapAdviceTypeLabel(row.adviceType, row.adviceTableName);
|
||||
existingRow.skinTestFlag = skinTestFlag;
|
||||
existingRow.skinTestFlag_enumText = skinTestFlag == 1 ? '是' : '否';
|
||||
existingRow.uniqueKey = preservedKey;
|
||||
existingRow.isEdit = preservedIsEdit;
|
||||
existingRow.statusEnum = preservedStatus;
|
||||
existingRow.showPopover = false;
|
||||
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
|
||||
// 🔧 Bug #455: 诊疗医嘱(adviceType=3)的执行科室默认使用患者就诊科室,
|
||||
// 不使用positionId(诊疗目录配置的执行科室),避免配置ID不在机构树中导致显示原始ID
|
||||
@@ -4489,7 +4518,7 @@ function escKeyListener(e) {
|
||||
index = prescriptionList.value.findIndex((item) => item.uniqueKey == expandOrder.value[0]);
|
||||
}
|
||||
if (index == 0) {
|
||||
updateExpandOrder([]);
|
||||
collapseAllExpanded();
|
||||
}
|
||||
prescriptionList.value.shift();
|
||||
isAdding.value = false;
|
||||
@@ -4500,7 +4529,7 @@ function escKeyListener(e) {
|
||||
|
||||
// 签退/撤回
|
||||
function handleSingOut() {
|
||||
let selectRows = prescriptionRef.value.getSelectionRows();
|
||||
let selectRows = prescriptionRef.value.getCheckboxRecords();
|
||||
console.log('BugFix#219: handleSingOut called, selectRows=', selectRows);
|
||||
console.log('BugFix#219: 选中行详情:', selectRows.map(item => ({
|
||||
adviceType: item.adviceType,
|
||||
@@ -4686,7 +4715,7 @@ function handleGroupId(paramList) {
|
||||
|
||||
// 组合
|
||||
function combination() {
|
||||
let selectRows = prescriptionRef.value.getSelectionRows();
|
||||
let selectRows = prescriptionRef.value.getCheckboxRecords();
|
||||
if (selectRows.length <= 1) {
|
||||
proxy.$modal.msgWarning('至少选择两项');
|
||||
return;
|
||||
@@ -4748,7 +4777,7 @@ function combination() {
|
||||
|
||||
// 拆组
|
||||
function split() {
|
||||
let selectRows = prescriptionRef.value.getSelectionRows();
|
||||
let selectRows = prescriptionRef.value.getCheckboxRecords();
|
||||
if (selectRows.length < 1) {
|
||||
proxy.$modal.msgWarning('至少选择一项');
|
||||
return;
|
||||
@@ -5342,8 +5371,8 @@ const orderSetDialogScope = ref('personal');
|
||||
function openOrderSetDialog(scope) {
|
||||
orderSetDialogScope.value = scope || 'personal';
|
||||
const selectedRaw =
|
||||
prescriptionRef.value && prescriptionRef.value.getSelectionRows
|
||||
? prescriptionRef.value.getSelectionRows()
|
||||
prescriptionRef.value && prescriptionRef.value.getCheckboxRecords
|
||||
? prescriptionRef.value.getCheckboxRecords()
|
||||
: [];
|
||||
|
||||
const selected = (selectedRaw || []).map((row) => {
|
||||
@@ -5435,6 +5464,8 @@ defineExpose({ getListInfo, getDiagnosisInfo });
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
@@ -5442,6 +5473,18 @@ defineExpose({ getListInfo, getDiagnosisInfo });
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.edit-form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.edit-form-row .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 调整element组件默认间距 */
|
||||
// .el-select,
|
||||
// .el-input-number {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.open"
|
||||
:model-value="props.open"
|
||||
@update:model-value="(val) => !val && emit('close')"
|
||||
title="退费单"
|
||||
width="1300px"
|
||||
teleported
|
||||
@@ -221,7 +222,7 @@ function tableSpanMethod({ row, column, rowIndex }) {
|
||||
|
||||
function submit() {
|
||||
// 1. 获取当前选中行并提取去重的 paymentId 列表
|
||||
const selectedRows = proxy.$refs['refundListRef'].getSelectionRows();
|
||||
const selectedRows = proxy.$refs['refundListRef'].getCheckboxRecords();
|
||||
const selectedPaymentIds = [...new Set(selectedRows.map((row) => row.paymentId))];
|
||||
|
||||
// 2. 遍历 refundList,筛选出符合条件的数据并设置 refundFlag
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
align="center"
|
||||
width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template #default>
|
||||
{{ '1' }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
@@ -166,7 +166,7 @@ const handleDeleteRow = (row) => {
|
||||
|
||||
// 提交处理
|
||||
const handleSubmit = () => {
|
||||
const selectedRows = consumableTableRef.value.getSelectionRows();
|
||||
const selectedRows = consumableTableRef.value.getCheckboxRecords();
|
||||
// 保存到本地存储
|
||||
// localStorage.setItem('doctor' + userStore.id.toString(), dontShowAgain.value);
|
||||
if (selectedRows.length === 0) {
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
<el-dialog
|
||||
v-model="open"
|
||||
:title="title"
|
||||
width="900px"
|
||||
width="1200px"
|
||||
teleported
|
||||
:close-on-click-modal="false"
|
||||
@close="cancel"
|
||||
@@ -397,7 +397,7 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 次要手术表格 -->
|
||||
<el-row v-if="form.secondarySurgeries && form.secondarySurgeries.length > 0">
|
||||
<el-row v-if="form.secondarySurgeries && form.secondarySurgeries.length > 0" style="margin-top: 12px;">
|
||||
<el-col
|
||||
:span="24"
|
||||
style="margin-bottom: 20px;"
|
||||
@@ -407,10 +407,12 @@
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:column-config="{ resizable: true }"
|
||||
>
|
||||
<vxe-column
|
||||
title="手术名称"
|
||||
min-width="200"
|
||||
min-width="250"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
@@ -1815,4 +1817,11 @@ defineExpose({
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* Bug #770: 确保对话框表单内容可滚动,防止操作按钮遮盖字段 */
|
||||
:deep(.el-dialog__body) {
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -193,13 +193,13 @@
|
||||
/>
|
||||
</div>
|
||||
<vxe-table
|
||||
ref="prescriptionRef"
|
||||
:ref="(el) => { if (el) tableRefs[pIndex] = el }"
|
||||
v-loading="loading"
|
||||
:data="prescription.prescriptionList"
|
||||
:row-config="{ keyField: 'uniqueKey', expandRowKeys: prescription.expandOrder }"
|
||||
border
|
||||
@cell-click="clickRow"
|
||||
@cell-dblclick="(row) => clickRowDb(row, pIndex)"
|
||||
@cell-dblclick="({ row }) => clickRowDb(row, pIndex)"
|
||||
>
|
||||
<vxe-column
|
||||
type="expand"
|
||||
@@ -217,8 +217,8 @@
|
||||
style="padding: 16px; background: #f8f9fa; border-radius: 8px"
|
||||
>
|
||||
<template v-if="scope.row.adviceType == 1">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 8px">
|
||||
<span class="medicine-title">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: nowrap">
|
||||
<span class="medicine-title" style="flex-shrink: 0">
|
||||
{{
|
||||
scope.row.adviceName +
|
||||
' ' +
|
||||
@@ -230,119 +230,45 @@
|
||||
']'
|
||||
}}
|
||||
</span>
|
||||
<el-form-item
|
||||
prop="lotNumber"
|
||||
label="药房:"
|
||||
>
|
||||
<el-select
|
||||
v-model="scope.row.inventoryId"
|
||||
style="width: 380px; margin-right: 20px"
|
||||
placeholder="药房"
|
||||
>
|
||||
<el-form-item prop="lotNumber" label="药房:" style="margin-bottom: 0; flex-shrink: 0">
|
||||
<el-select v-model="scope.row.inventoryId" style="width: 280px" placeholder="药房">
|
||||
<el-option
|
||||
v-for="item in scope.row.stockList"
|
||||
:key="item.inventoryId"
|
||||
:value="item.inventoryId"
|
||||
:label="
|
||||
item.locationName +
|
||||
' ' +
|
||||
'批次号: ' +
|
||||
item.lotNumber +
|
||||
' ' +
|
||||
' 库存:' +
|
||||
item.quantity / scope.row.partPercent +
|
||||
item.unitCode_dictText +
|
||||
' 单价:' +
|
||||
item.price.toFixed(2) +
|
||||
'/' +
|
||||
item.unitCode_dictText
|
||||
"
|
||||
:label="item.locationName + ' 库存:' + item.quantity / scope.row.partPercent + item.unitCode_dictText + ' 单价:' + item.price.toFixed(2) + '/' + item.unitCode_dictText"
|
||||
@click="handleNumberClick(item, scope.rowIndex, pIndex)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="form-group">
|
||||
<!-- 单次剂量 -->
|
||||
<el-form-item
|
||||
label="数量:"
|
||||
prop="minUnitQuantity"
|
||||
class="required-field"
|
||||
data-prop="minUnitQuantity"
|
||||
>
|
||||
<el-input-number
|
||||
:ref="(el) => (inputRefs.minUnitQuantity = el)"
|
||||
v-model="scope.row.minUnitQuantity"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
:controls="false"
|
||||
style="width: 70px; margin-right: 20px"
|
||||
@input="
|
||||
() => {
|
||||
nextTick(() => {
|
||||
scope.row.totalPrice =
|
||||
scope.row.minUnitQuantity * scope.row.unitPrice;
|
||||
});
|
||||
}
|
||||
"
|
||||
@keyup.enter.prevent="
|
||||
handleEnter('minUnitQuantity', scope.row, scope.rowIndex, pIndex)
|
||||
"
|
||||
<el-form-item label="数量:" prop="minUnitQuantity" class="required-field" data-prop="minUnitQuantity" style="margin-bottom: 0; flex-shrink: 0">
|
||||
<el-input-number
|
||||
:ref="(el) => (inputRefs.minUnitQuantity = el)"
|
||||
v-model="scope.row.minUnitQuantity"
|
||||
:min="0" controls-position="right" :controls="false" style="width: 75px"
|
||||
@input="() => { nextTick(() => { scope.row.totalPrice = scope.row.minUnitQuantity * scope.row.unitPrice; }); }"
|
||||
@keyup.enter.prevent="handleEnter('minUnitQuantity', scope.row, scope.rowIndex, pIndex)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-select v-model="scope.row.minUnitCode" style="width: 75px; flex-shrink: 0" placeholder=" ">
|
||||
<template v-for="item in scope.row.unitCodeList" :key="item.value">
|
||||
<el-option
|
||||
v-if="scope.row.unitCodeList.length == 3 ? item.type == unitMap['minUnit'] : item.type == unitMap['unit']"
|
||||
:value="item.value" :label="item.label"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 剂量单位 -->
|
||||
<el-select
|
||||
v-model="scope.row.minUnitCode"
|
||||
style="width: 80px; margin-right: 20px"
|
||||
placeholder=" "
|
||||
>
|
||||
<template
|
||||
v-for="item in scope.row.unitCodeList"
|
||||
:key="item.value"
|
||||
>
|
||||
<el-option
|
||||
v-if="
|
||||
scope.row.unitCodeList.length == 3
|
||||
? item.type == unitMap['minUnit']
|
||||
: item.type == unitMap['unit']
|
||||
"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</template>
|
||||
</el-select>
|
||||
<el-form-item label="特殊煎法:" prop="dosageInstruction" class="required-field" data-prop="dosageInstruction" style="margin-bottom: 0; flex-shrink: 0">
|
||||
<el-select v-model="scope.row.dosageInstruction" style="width: 95px" placeholder=" ">
|
||||
<template v-for="item in dosage_instruction" :key="item.value">
|
||||
<el-option :value="item.value" :label="item.label" />
|
||||
</template>
|
||||
</el-select>
|
||||
<!-- 单次剂量 -->
|
||||
<el-form-item
|
||||
label="特殊煎法:"
|
||||
prop="dosageInstruction"
|
||||
class="required-field"
|
||||
data-prop="dosageInstruction"
|
||||
>
|
||||
<el-select
|
||||
v-model="scope.row.dosageInstruction"
|
||||
style="width: 90px; margin-right: 20px"
|
||||
placeholder=" "
|
||||
>
|
||||
<template
|
||||
v-for="item in dosage_instruction"
|
||||
:key="item.value"
|
||||
>
|
||||
<el-option
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<span class="total-amount">
|
||||
总金额:{{
|
||||
scope.row.totalPrice ? scope.row.totalPrice + ' 元' : '0.00 元'
|
||||
}}
|
||||
</el-form-item>
|
||||
<span class="total-amount" style="flex-shrink: 0; white-space: nowrap">
|
||||
总金额:{{ scope.row.totalPrice ? scope.row.totalPrice + ' 元' : '0.00 元' }}
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveSign(scope.row, scope.rowIndex, pIndex)"
|
||||
>
|
||||
<el-button type="primary" style="flex-shrink: 0" @click="handleSaveSign(scope.row, scope.rowIndex, pIndex)">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -394,7 +320,7 @@
|
||||
:width="1200"
|
||||
>
|
||||
<tcmMedicineList
|
||||
ref="adviceTableRef"
|
||||
:ref="(el) => { if (el) activeAdviceTable = el }"
|
||||
:popover-visible="scope.row.showPopover"
|
||||
:advice-query-params="adviceQueryParams"
|
||||
:patient-info="props.patientInfo"
|
||||
@@ -416,7 +342,7 @@
|
||||
if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
// 传递事件到弹窗容器
|
||||
adviceTableRef.handleKeyDown(e);
|
||||
activeAdviceTable?.handleKeyDown(e);
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -602,7 +528,7 @@ const tcmPrescriptionList = ref([
|
||||
},
|
||||
]);
|
||||
const unitCodeList = ref([]);
|
||||
const adviceTableRef = ref([]);
|
||||
const activeAdviceTable = ref(null);
|
||||
const organization = ref([]);
|
||||
const loading = ref(false);
|
||||
const rowRules = ref({
|
||||
@@ -625,7 +551,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const prescriptionRef = ref();
|
||||
const tableRefs = ref({});
|
||||
const stockList = ref([]);
|
||||
const contractList = ref([]);
|
||||
const tcmDiagnosisList = ref([]);
|
||||
@@ -842,7 +768,7 @@ function handleAddMedicine(pIndex) {
|
||||
/**
|
||||
* 点击行赋值
|
||||
*/
|
||||
function clickRow(row) {
|
||||
function clickRow({ row }) {
|
||||
emit('selectDiagnosis', row);
|
||||
}
|
||||
|
||||
@@ -856,7 +782,7 @@ function clickRowDb(row, pIndex) {
|
||||
const index = prescription.prescriptionList.findIndex(
|
||||
(item) => item.uniqueKey === row.uniqueKey
|
||||
);
|
||||
prescription.prescriptionList[index] = row;
|
||||
prescription.prescriptionList.splice(index, 1, row);
|
||||
prescription.expandOrder = [row.uniqueKey];
|
||||
}
|
||||
}
|
||||
@@ -873,8 +799,14 @@ function handleFocus(row, index, pIndex) {
|
||||
row.showPopover = true;
|
||||
}
|
||||
|
||||
let blurTimer = null;
|
||||
|
||||
function handleBlur(row) {
|
||||
row.showPopover = false;
|
||||
if (blurTimer) clearTimeout(blurTimer);
|
||||
blurTimer = setTimeout(() => {
|
||||
row.showPopover = false;
|
||||
blurTimer = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
@@ -995,96 +927,59 @@ function getPrescriptionMedicineCount(prescriptionIndex) {
|
||||
* 选择药品回调
|
||||
*/
|
||||
function selectAdviceBase(key, row, pIndex) {
|
||||
// ... 保持原有逻辑,但使用对应处方的数据 ...
|
||||
const prescription = tcmPrescriptionList.value[pIndex];
|
||||
getOrgList();
|
||||
unitCodeList.value = [];
|
||||
unitCodeList.value.push({ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' });
|
||||
unitCodeList.value.push({ value: row.unitCode, label: row.unitCode_dictText, type: "unit" });
|
||||
if (row.doseUnitCode != row.minUnitCode) {
|
||||
unitCodeList.value.push({
|
||||
value: row.doseUnitCode,
|
||||
label: row.doseUnitCode_dictText,
|
||||
type: 'dose',
|
||||
});
|
||||
unitCodeList.value.push({ value: row.doseUnitCode, label: row.doseUnitCode_dictText, type: "dose" });
|
||||
}
|
||||
if (
|
||||
(row.partAttributeEnum == 1 || row.partAttributeEnum == 3) &&
|
||||
row.minUnitCode != row.unitCode
|
||||
) {
|
||||
unitCodeList.value.push({
|
||||
value: row.minUnitCode,
|
||||
label: row.minUnitCode_dictText,
|
||||
type: 'minUnit',
|
||||
});
|
||||
if ((row.partAttributeEnum == 1 || row.partAttributeEnum == 3) && row.minUnitCode != row.unitCode) {
|
||||
unitCodeList.value.push({ value: row.minUnitCode, label: row.minUnitCode_dictText, type: "minUnit" });
|
||||
}
|
||||
if (row.adviceType == 2 && row.minUnitCode != row.unitCode) {
|
||||
unitCodeList.value.push({
|
||||
value: row.minUnitCode,
|
||||
label: row.minUnitCode_dictText,
|
||||
type: 'minUnit',
|
||||
});
|
||||
unitCodeList.value.push({ value: row.minUnitCode, label: row.minUnitCode_dictText, type: "minUnit" });
|
||||
}
|
||||
// vxe-table v4: keep row ref, mutate in-place (ref prescriptionlist.vue setValue)
|
||||
const existingRow = prescription.prescriptionList[rowIndex.value];
|
||||
if (!existingRow) return;
|
||||
const pk = existingRow.uniqueKey, pe = existingRow.isEdit, pg = existingRow.groupId;
|
||||
Object.keys(existingRow).forEach(k => { delete existingRow[k]; });
|
||||
const rowData = JSON.parse(JSON.stringify(row));
|
||||
prescription.prescriptionList[rowIndex.value] = {
|
||||
...prescription.prescriptionList[rowIndex.value], // 保留原有属性(如 uniqueKey, groupId)
|
||||
...rowData, // 覆盖药品信息
|
||||
statusEnum: 1, // 【关键修复】强制状态为“待签发/草稿”,确保能被保存过滤器捕获
|
||||
adviceDefinitionId: rowData.adviceDefinitionId // 确保这个字段有值,后端会根据这个字段过滤空行
|
||||
};
|
||||
prescription.prescriptionList[rowIndex.value].orgId = undefined;
|
||||
prescription.prescriptionList[rowIndex.value].dose = undefined;
|
||||
prescription.prescriptionList[rowIndex.value].unitCodeList = unitCodeList.value;
|
||||
prescription.prescriptionList[rowIndex.value].doseUnitCode = row.doseUnitCode;
|
||||
prescription.prescriptionList[rowIndex.value].minUnitCode = row.minUnitCode;
|
||||
prescription.prescriptionList[rowIndex.value].unitCode =
|
||||
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
|
||||
prescription.prescriptionList[rowIndex.value].definitionId = JSON.parse(
|
||||
JSON.stringify(row)
|
||||
).chargeItemDefinitionId;
|
||||
|
||||
// 库存列表 + 价格列表拼成批次号的下拉框
|
||||
Object.assign(existingRow, rowData);
|
||||
existingRow.uniqueKey = pk; existingRow.isEdit = pe; existingRow.groupId = pg;
|
||||
existingRow.statusEnum = 1; existingRow.showPopover = false;
|
||||
existingRow.adviceDefinitionId = rowData.adviceDefinitionId;
|
||||
existingRow.orgId = undefined; existingRow.dose = undefined;
|
||||
existingRow.unitCodeList = unitCodeList.value;
|
||||
existingRow.doseUnitCode = row.doseUnitCode;
|
||||
existingRow.minUnitCode = row.minUnitCode;
|
||||
existingRow.unitCode = row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
|
||||
existingRow.definitionId = JSON.parse(JSON.stringify(row)).chargeItemDefinitionId;
|
||||
if (row.adviceType == 1 || row.adviceType == 2) {
|
||||
if (row.inventoryList && row.inventoryList.length == 0) {
|
||||
prescription.expandOrder = [];
|
||||
proxy.$modal.msgWarning('该项目无库存');
|
||||
return;
|
||||
prescription.expandOrder = []; proxy.$modal.msgWarning("无库存"); return;
|
||||
}
|
||||
stockList.value = row.inventoryList.map((item, index) => {
|
||||
return { ...item, ...row.priceList[index] };
|
||||
});
|
||||
prescription.prescriptionList[rowIndex.value].stockList = stockList.value;
|
||||
// 获取默认批次号的库存,如果没有让医生重新选
|
||||
let stock = stockList.value.filter((item) => {
|
||||
return item.lotNumber == row.defaultLotNumber;
|
||||
})[0];
|
||||
if (stock != {} && stock != undefined) {
|
||||
if (stock.quantity <= 0) {
|
||||
proxy.$modal.msgWarning('该项目库存不足,请选择其它库房');
|
||||
// return;
|
||||
}
|
||||
prescription.prescriptionList[rowIndex.value].lotNumber = stock.lotNumber;
|
||||
prescription.prescriptionList[rowIndex.value].inventoryId = stock.inventoryId;
|
||||
prescription.prescriptionList[rowIndex.value].locationId = stock.locationId;
|
||||
prescription.prescriptionList[rowIndex.value].unitPrice = stock.price;
|
||||
prescription.prescriptionList[rowIndex.value].positionName = stock.locationName;
|
||||
stockList.value = row.inventoryList.map((item, i) => ({ ...item, ...row.priceList[i] }));
|
||||
existingRow.stockList = stockList.value;
|
||||
const s = stockList.value.filter(it => it.lotNumber == row.defaultLotNumber)[0];
|
||||
if (s && Object.keys(s).length > 0) {
|
||||
if (s.quantity <= 0) proxy.$modal.msgWarning("库存不足");
|
||||
existingRow.lotNumber = s.lotNumber; existingRow.inventoryId = s.inventoryId;
|
||||
existingRow.locationId = s.locationId; existingRow.unitPrice = s.price;
|
||||
existingRow.positionName = s.locationName;
|
||||
}
|
||||
} else {
|
||||
prescription.prescriptionList[rowIndex.value].orgId = JSON.parse(
|
||||
JSON.stringify(row)
|
||||
).positionId;
|
||||
prescription.prescriptionList[rowIndex.value].unitPrice = row.priceList[0].price;
|
||||
existingRow.orgId = JSON.parse(JSON.stringify(row)).positionId;
|
||||
existingRow.unitPrice = row.priceList[0].price;
|
||||
}
|
||||
prescription.expandOrder = [key];
|
||||
nextTick(() => {
|
||||
const t = tableRefs.value[pIndex];
|
||||
if (t && t.setRowExpand) t.setRowExpand([existingRow], true);
|
||||
if (row.adviceType == 1) {
|
||||
if (row.injectFlag == 1) {
|
||||
inputRefs.value['executeNum']?.focus();
|
||||
} else {
|
||||
inputRefs.value['dose']?.focus();
|
||||
}
|
||||
} else {
|
||||
inputRefs.value['quantity']?.focus();
|
||||
}
|
||||
row.injectFlag == 1 ? inputRefs.value.executeNum?.focus() : inputRefs.value.dose?.focus();
|
||||
} else { inputRefs.value.quantity?.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1246,6 +1141,12 @@ function handleSaveSign(row, index, pIndex) {
|
||||
}
|
||||
|
||||
row.contentJson = JSON.stringify(row);
|
||||
// 强制刷新数组引用,触发 vxe-table 重渲染(切换只读模式 + 关闭展开行)
|
||||
prescription.prescriptionList = [...prescription.prescriptionList];
|
||||
nextTick(() => {
|
||||
const t = tableRefs.value[pIndex];
|
||||
if (t && t.clearRowExpand) t.clearRowExpand();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1345,6 +1246,8 @@ function escKeyListener(e) {
|
||||
}
|
||||
prescription.prescriptionList.shift();
|
||||
prescription.isAdding = false;
|
||||
// 强制刷新数组引用,触发 vxe-table 重渲染
|
||||
prescription.prescriptionList = [...prescription.prescriptionList];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
<div style="padding: 10px; position: relative">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
v-loading="loading"
|
||||
type="card"
|
||||
style="width: 100%; height: 100%"
|
||||
@tab-change="handleClick(activeTab)"
|
||||
@@ -546,7 +545,6 @@ const diagnosisRef = ref();
|
||||
const consultationRef = ref();
|
||||
const infectiousReportRef = ref();
|
||||
const waitCount = ref(0);
|
||||
const loading = ref(false);
|
||||
const { proxy } = getCurrentInstance();
|
||||
const visitType = ref('');
|
||||
const firstVisitDate = ref('');
|
||||
@@ -807,7 +805,6 @@ function handleCardClick(item, index) {
|
||||
currentEncounterId.value = item.encounterId;
|
||||
console.log('currentEncounterId.value 设置为:', currentEncounterId.value);
|
||||
|
||||
loading.value = true;
|
||||
patientList.value.forEach((patient) => {
|
||||
patient.active = patient.encounterId === item.encounterId;
|
||||
});
|
||||
@@ -860,9 +857,6 @@ function handleCardClick(item, index) {
|
||||
eprescriptionRef.value.getList();
|
||||
consultationRef.value.fetchConsultationList();
|
||||
// emrRef.value.getDetail(item.encounterId);
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// EMPI基础操作
|
||||
export function registerPerson(data) { return request({ url: '/api/v1/empi/person', method: 'post', data }) }
|
||||
export function mergePersons(primaryId, secondaryIds) { return request({ url: '/api/v1/empi/merge', method: 'post', params: { primaryId, secondaryIds: secondaryIds.join(',') } }) }
|
||||
export function findByGlobalId(globalId) { return request({ url: '/api/v1/empi/person/global/' + globalId, method: 'get' }) }
|
||||
export function findByIdCard(idCardNo) { return request({ url: '/api/v1/empi/person/idcard/' + idCardNo, method: 'get' }) }
|
||||
export function getMappings(globalId) { return request({ url: '/api/v1/empi/mappings/' + globalId, method: 'get' }) }
|
||||
export function getStatistics() { return request({ url: '/api/v1/empi/statistics', method: 'get' }) }
|
||||
export function listPersons(params) { return request({ url: '/api/v1/empi/persons', method: 'get', params }) }
|
||||
|
||||
// 关联院内患者查询
|
||||
export function findLinkedPatientsByGlobalId(globalId) { return request({ url: '/api/v1/empi/linked-patients/global/' + globalId, method: 'get' }) }
|
||||
export function findLinkedPatientsByIdCard(idCardNo) { return request({ url: '/api/v1/empi/linked-patients/idcard/' + idCardNo, method: 'get' }) }
|
||||
|
||||
// EMPI增强功能
|
||||
export function getPhotos(patientId) { return request({ url: '/empi-enhanced/photo/list', method: 'get', params: { patientId } }) }
|
||||
export function addPhoto(data) { return request({ url: '/empi-enhanced/photo/add', method: 'post', data }) }
|
||||
export function getFamilyMembers(patientId) { return request({ url: '/empi-enhanced/family/list', method: 'get', params: { patientId } }) }
|
||||
@@ -14,4 +21,4 @@ export function addFamilyMember(data) { return request({ url: '/empi-enhanced/fa
|
||||
export function deleteFamilyMember(id) { return request({ url: '/empi-enhanced/family/delete', method: 'delete', params: { id } }) }
|
||||
export function getMergeLogPage(params) { return request({ url: '/empi-enhanced/merge-log/page', method: 'get', params }) }
|
||||
export function addMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/add', method: 'post', data }) }
|
||||
export function undoMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/undo', method: 'post', data }) }
|
||||
export function undoMergeLog(data) { return request({ url: '/empi-enhanced/merge-log/undo', method: 'post', data }) }
|
||||
@@ -1,29 +1,105 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card>
|
||||
<template #header><span>患者合并管理</span></template>
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>患者合并管理</span>
|
||||
<el-button type="primary" :disabled="!primaryPatient" @click="handleMerge">
|
||||
合并选中患者 ({{ selectedRows.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" class="mb8">
|
||||
<el-form-item label="主患者ID"><el-input v-model="primaryId" placeholder="主患者ID" /></el-form-item>
|
||||
<el-form-item label="待合并ID"><el-input v-model="secondaryIds" placeholder="逗号分隔多个ID" /></el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="searchName" placeholder="患者姓名" clearable @keyup.enter="loadPatients" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号">
|
||||
<el-input v-model="searchIdCard" placeholder="身份证号" clearable @keyup.enter="loadPatients" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleMerge">合并</el-button>
|
||||
<el-button type="primary" @click="loadPatients">查询</el-button>
|
||||
<el-button @click="searchName=''; searchIdCard=''; loadPatients()">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="mt8">
|
||||
<template #header><span>合并日志</span></template>
|
||||
<el-table v-loading="loading" :data="mergeLogs">
|
||||
<el-table-column label="主患者" prop="primaryPatientName" width="120" />
|
||||
<el-table-column label="被合并患者" prop="secondaryPatientName" width="120" />
|
||||
<el-table-column label="合并原因" prop="mergeReason" width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" prop="operatorName" width="100" />
|
||||
<el-table-column label="合并时间" prop="mergeTime" width="170" />
|
||||
<el-table-column label="状态" prop="status" width="90">
|
||||
<template #default="s"><el-tag :type="s.row.status==='ACTIVE'?'success':'info'">{{ s.row.status === 'ACTIVE' ? '有效' : '已撤销' }}</el-tag></template>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px">
|
||||
先点击"设为主患者"选一个保留的,再勾选要合并的其他患者,最后点右上角合并按钮
|
||||
</el-alert>
|
||||
<el-table v-loading="loading" :data="patientList" border stripe @selection-change="handleSelectionChange" row-key="id">
|
||||
<el-table-column type="selection" width="50" :selectable="canSelect" />
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="globalId" label="全局ID" width="160" />
|
||||
<el-table-column prop="name" label="姓名" width="100">
|
||||
<template #default="{row}">
|
||||
<span :style="primaryPatient && primaryPatient.id === row.id ? 'color:#409eff;font-weight:bold' : ''">
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="60">
|
||||
<template #default="{row}">{{ row.gender === 'M' ? '男' : row.gender === 'F' ? '女' : row.gender }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthDate" label="出生日期" width="110">
|
||||
<template #default="{row}">{{ row.birthDate ? row.birthDate.substring(0,10) : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="idCardNo" label="身份证号" width="180" />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="mergeStatus" label="状态" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.mergeStatus === 'ACTIVE' ? 'success' : row.mergeStatus === 'MERGED' ? 'info' : 'warning'" size="small">
|
||||
{{ row.mergeStatus === 'ACTIVE' ? '正常' : row.mergeStatus === 'MERGED' ? '已合并' : '待处理' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="s">
|
||||
<el-button link type="warning" v-if="s.row.status==='ACTIVE'" @click="handleUndo(s.row)">撤销</el-button>
|
||||
<template #default="{row}">
|
||||
<el-button link type="primary" size="small"
|
||||
:type="primaryPatient && primaryPatient.id === row.id ? 'success' : ''"
|
||||
@click="setPrimary(row)">
|
||||
{{ primaryPatient && primaryPatient.id === row.id ? '已选为主' : '设为主患者' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="confirmVisible" title="确认合并" width="500px">
|
||||
<el-descriptions title="主患者(保留)" :column="2" border size="small">
|
||||
<el-descriptions-item label="姓名">{{ primaryPatient?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="全局ID">{{ primaryPatient?.globalId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证">{{ primaryPatient?.idCardNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ primaryPatient?.phone }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<p style="font-weight:bold;margin-bottom:8px">将被合并的患者({{ selectedRows.length }}人):</p>
|
||||
<el-table :data="selectedRows" border size="small" max-height="200">
|
||||
<el-table-column prop="name" label="姓名" width="80" />
|
||||
<el-table-column prop="globalId" label="全局ID" width="160" />
|
||||
<el-table-column prop="idCardNo" label="身份证号" width="180" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="confirmVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="doMerge">确认合并</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-card style="margin-top:16px">
|
||||
<template #header><span>合并日志</span></template>
|
||||
<el-table v-loading="logLoading" :data="mergeLogs" border stripe>
|
||||
<el-table-column label="主患者" prop="sourcePatientId" width="120">
|
||||
<template #default="{row}">{{ getPatientName(row.sourcePatientId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="被合并" prop="targetPatientId" width="120">
|
||||
<template #default="{row}">{{ getPatientName(row.targetPatientId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" prop="mergeType" width="80" />
|
||||
<el-table-column label="原因" prop="mergeReason" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" prop="mergeBy" width="90" />
|
||||
<el-table-column label="时间" prop="mergeTime" width="170" />
|
||||
<el-table-column label="状态" prop="status" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status==='MERGED'?'success':'info'" size="small">
|
||||
{{ row.status === 'MERGED' ? '已合并' : '已撤回' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -33,22 +109,73 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { mergePersons, getMergeLogPage, undoMergeLog } from '../api'
|
||||
import { mergePersons, getMergeLogPage, listPersons } from '../api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false); const mergeLogs = ref([])
|
||||
const primaryId = ref(''); const secondaryIds = ref('')
|
||||
const loading = ref(false)
|
||||
const logLoading = ref(false)
|
||||
const patientList = ref([])
|
||||
const mergeLogs = ref([])
|
||||
const searchName = ref('')
|
||||
const searchIdCard = ref('')
|
||||
const primaryPatient = ref(null)
|
||||
const selectedRows = ref([])
|
||||
const confirmVisible = ref(false)
|
||||
const tableRef = ref(null)
|
||||
|
||||
const loadLogs = async () => { loading.value = true; const res = await getMergeLogPage({ pageNo: 1, pageSize: 20 }); mergeLogs.value = res.data?.records || []; loading.value = false }
|
||||
const handleMerge = async () => {
|
||||
if (!primaryId.value || !secondaryIds.value) { ElMessage.warning('请填写ID'); return }
|
||||
await ElMessageBox.confirm('确认合并?', '提示', { type: 'warning' })
|
||||
await mergePersons(primaryId.value, secondaryIds.value.split(',').map(Number))
|
||||
ElMessage.success('合并成功'); primaryId.value = ''; secondaryIds.value = ''; loadLogs()
|
||||
const loadPatients = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listPersons({ name: searchName.value, idCardNo: searchIdCard.value })
|
||||
patientList.value = res.data || []
|
||||
} catch (e) {
|
||||
patientList.value = []
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
const handleUndo = async (row) => {
|
||||
await ElMessageBox.confirm('确认撤销合并?', '提示', { type: 'warning' })
|
||||
await undoMergeLog({ id: row.id, operatorName: '管理员' }); ElMessage.success('已撤销'); loadLogs()
|
||||
|
||||
const loadLogs = async () => {
|
||||
logLoading.value = true
|
||||
const res = await getMergeLogPage({ pageNo: 1, pageSize: 20 })
|
||||
mergeLogs.value = res.data?.records || []
|
||||
logLoading.value = false
|
||||
}
|
||||
onMounted(() => loadLogs())
|
||||
</script>
|
||||
|
||||
const canSelect = (row) => row.mergeStatus !== 'MERGED'
|
||||
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection.filter(r => r.id !== primaryPatient.value?.id)
|
||||
}
|
||||
|
||||
const setPrimary = (row) => {
|
||||
primaryPatient.value = row
|
||||
ElMessage.info('主患者已设为: ' + row.name)
|
||||
}
|
||||
|
||||
const handleMerge = () => {
|
||||
if (!primaryPatient.value) { ElMessage.warning('请先选择主患者'); return }
|
||||
if (selectedRows.value.length === 0) { ElMessage.warning('请勾选要合并的患者'); return }
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const doMerge = async () => {
|
||||
try {
|
||||
await mergePersons(primaryPatient.value.id, selectedRows.value.map(r => r.id))
|
||||
ElMessage.success('合并成功')
|
||||
confirmVisible.value = false
|
||||
primaryPatient.value = null
|
||||
selectedRows.value = []
|
||||
loadPatients()
|
||||
loadLogs()
|
||||
} catch (e) {
|
||||
ElMessage.error('合并失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getPatientName = (id) => {
|
||||
const p = patientList.value.find(x => x.id === id)
|
||||
return p ? p.name : id
|
||||
}
|
||||
|
||||
onMounted(() => { loadPatients(); loadLogs() })
|
||||
</script>
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="总患者数" :value="stats.totalPatients || 0" /></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已合并" :value="stats.mergedPatients || 0" /></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="总患者数" :value="stats.totalPersons || 0" /></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已合并" :value="stats.mergedPersons || 0" /></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="待合并" :value="stats.pendingMerges || 0" /></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><el-statistic title="重复率" :value="stats.duplicateRate || 0" suffix="%" /></el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
@@ -13,30 +14,97 @@
|
||||
<el-button type="primary" icon="Plus" @click="handleRegister">注册患者</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="searchForm" :inline="true" class="mb8">
|
||||
<el-form-item label="全局ID"><el-input v-model="searchForm.globalId" placeholder="全局ID" clearable /></el-form-item>
|
||||
<el-form-item label="身份证号"><el-input v-model="searchForm.idCardNo" placeholder="身份证号" clearable /></el-form-item>
|
||||
<el-form-item label="姓名"><el-input v-model="searchForm.name" placeholder="患者姓名" clearable @keyup.enter="handleSearch" /></el-form-item>
|
||||
<el-form-item label="身份证号"><el-input v-model="searchForm.idCardNo" placeholder="身份证号" clearable @keyup.enter="handleSearch" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="searchForm={};patientData=null">重置</el-button>
|
||||
<el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions v-if="patientData" :column="2" border>
|
||||
|
||||
<el-table :data="patientList" border stripe v-loading="loading" @row-click="handleRowClick" highlight-current-row>
|
||||
<el-table-column prop="globalId" label="全局ID" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="gender" label="性别" width="60">
|
||||
<template #default="{row}">{{ row.gender === 'M' ? '男' : row.gender === 'F' ? '女' : row.gender === '1' ? '男' : row.gender === '2' ? '女' : row.gender }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthDate" label="出生日期" width="120" />
|
||||
<el-table-column prop="idCardNo" label="身份证号" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="sourceSystem" label="来源" width="80" />
|
||||
<el-table-column prop="mergeStatus" label="状态" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.mergeStatus === 'ACTIVE' ? 'success' : 'warning'" size="small">
|
||||
{{ row.mergeStatus === 'ACTIVE' ? '正常' : row.mergeStatus === 'MERGED' ? '已合并' : row.mergeStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 患者详情 -->
|
||||
<el-card v-if="patientData" style="margin-top:16px">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>患者详情</span>
|
||||
<el-button text @click="patientData = null; linkedPatients = []; mappings = []">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="全局ID">{{ patientData.globalId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ patientData.patientName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ patientData.gender === 'M' ? '男' : '女' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ patientData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ patientData.gender === 'M' ? '男' : patientData.gender === 'F' ? '女' : patientData.gender === '1' ? '男' : patientData.gender === '2' ? '女' : patientData.gender }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出生日期">{{ patientData.birthDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ patientData.idCardNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ patientData.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ patientData.address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源系统">{{ patientData.sourceSystem }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="patientData.mergeStatus === 'ACTIVE' ? 'success' : 'warning'" size="small">
|
||||
{{ patientData.mergeStatus === 'ACTIVE' ? '正常' : '已合并' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="请输入查询条件" />
|
||||
</el-card>
|
||||
|
||||
<!-- 关联院内患者记录 -->
|
||||
<el-card v-if="linkedPatients.length > 0" style="margin-top:16px">
|
||||
<template #header><span>关联院内患者记录 ({{ linkedPatients.length }})</span></template>
|
||||
<el-table :data="linkedPatients" border stripe>
|
||||
<el-table-column prop="id" label="院内ID" width="100" />
|
||||
<el-table-column prop="busNo" label="病历号" width="140" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="genderEnum" label="性别" width="60">
|
||||
<template #default="{row}">{{ row.genderEnum === 1 ? '男' : '女' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthDate" label="出生日期" width="120">
|
||||
<template #default="{row}">{{ row.birthDate ? row.birthDate.substring(0,10) : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="idCard" label="身份证号" width="180" />
|
||||
<el-table-column prop="address" label="地址" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ID映射列表 -->
|
||||
<el-card v-if="mappings.length > 0" style="margin-top:16px">
|
||||
<template #header><span>ID映射关系</span></template>
|
||||
<el-table :data="mappings" border stripe>
|
||||
<el-table-column prop="globalId" label="全局ID" width="180" />
|
||||
<el-table-column prop="localPatientId" label="院内患者ID" width="120" />
|
||||
<el-table-column prop="sourceSystem" label="来源系统" width="100" />
|
||||
<el-table-column prop="idType" label="标识类型" width="100" />
|
||||
<el-table-column prop="idValue" label="标识值" min-width="150" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="注册患者" v-model="dialogVisible" width="600px">
|
||||
<el-form :model="formData" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12"><el-form-item label="姓名"><el-input v-model="formData.patientName" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="姓名"><el-input v-model="formData.name" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="性别">
|
||||
<el-select v-model="formData.gender"><el-option v-for="d in sys_user_sex" :key="d.value" :label="d.label" :value="d.value" /></el-select>
|
||||
</el-form-item></el-col>
|
||||
@@ -57,23 +125,68 @@
|
||||
<script setup>
|
||||
import { useDict } from '@/utils/dict'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { registerPerson, findByGlobalId, findByIdCard, getStatistics } from '../api'
|
||||
import {
|
||||
registerPerson, findByGlobalId, findByIdCard, getStatistics, listPersons,
|
||||
findLinkedPatientsByGlobalId, findLinkedPatientsByIdCard, getMappings
|
||||
} from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const { sys_user_sex } = useDict('sys_user_sex')
|
||||
const stats = ref({})
|
||||
const searchForm = reactive({ globalId: '', idCardNo: '' })
|
||||
const loading = ref(false)
|
||||
const patientList = ref([])
|
||||
const searchForm = reactive({ name: '', idCardNo: '' })
|
||||
const patientData = ref(null)
|
||||
const linkedPatients = ref([])
|
||||
const mappings = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const formData = ref({})
|
||||
|
||||
const loadStats = async () => { const res = await getStatistics(); stats.value = res.data || {} }
|
||||
const handleSearch = async () => {
|
||||
if (searchForm.globalId) { const res = await findByGlobalId(searchForm.globalId); patientData.value = res.data }
|
||||
else if (searchForm.idCardNo) { const res = await findByIdCard(searchForm.idCardNo); patientData.value = res.data }
|
||||
else { ElMessage.warning('请输入查询条件') }
|
||||
|
||||
const loadPatientList = async (params = {}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listPersons(params)
|
||||
patientList.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
patientData.value = null
|
||||
linkedPatients.value = []
|
||||
mappings.value = []
|
||||
const params = {}
|
||||
if (searchForm.name) params.name = searchForm.name
|
||||
if (searchForm.idCardNo) params.idCardNo = searchForm.idCardNo
|
||||
await loadPatientList(params)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.name = ''
|
||||
searchForm.idCardNo = ''
|
||||
patientData.value = null
|
||||
linkedPatients.value = []
|
||||
mappings.value = []
|
||||
loadPatientList()
|
||||
}
|
||||
|
||||
const handleRowClick = async (row) => {
|
||||
patientData.value = row
|
||||
linkedPatients.value = []
|
||||
mappings.value = []
|
||||
try {
|
||||
const lp = await findLinkedPatientsByGlobalId(row.globalId)
|
||||
linkedPatients.value = lp.data || []
|
||||
const mp = await getMappings(row.globalId)
|
||||
mappings.value = mp.data || []
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
const handleRegister = () => { formData.value = {}; dialogVisible.value = true }
|
||||
const submitForm = async () => { await registerPerson(formData.value); ElMessage.success('注册成功'); dialogVisible.value = false; loadStats() }
|
||||
onMounted(() => loadStats())
|
||||
</script>
|
||||
const submitForm = async () => { await registerPerson(formData.value); ElMessage.success('注册成功'); dialogVisible.value = false; loadStats(); loadPatientList() }
|
||||
|
||||
onMounted(() => { loadStats(); loadPatientList() })
|
||||
</script>
|
||||
@@ -33,6 +33,7 @@
|
||||
class="tree-card"
|
||||
>
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
ref="treeRef"
|
||||
:data="menuTree"
|
||||
:props="treeProps"
|
||||
@@ -189,8 +190,7 @@ import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Sortable from 'sortablejs'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { listMenu } from '@/api/system/menu'
|
||||
import { getMenuFullPath } from '@/api/system/menu'
|
||||
import { getUserMenus } from '@/api/system/menu'
|
||||
import { saveCurrentUserConfig, getCurrentUserConfig } from '@/api/system/userConfig'
|
||||
import {
|
||||
Menu,
|
||||
@@ -265,6 +265,7 @@ import SvgIcon from '@/components/SvgIcon'
|
||||
const loading = ref(false)
|
||||
|
||||
const treeRef = ref()
|
||||
const treeKey = ref(0)
|
||||
const menuTree = ref([])
|
||||
const expandedKeys = ref([])
|
||||
const checkedKeys = ref([])
|
||||
@@ -310,14 +311,17 @@ const loadMenuData = async () => {
|
||||
// 尝试从本地缓存获取菜单数据
|
||||
const cachedMenuData = localStorage.getItem('menuTreeCache');
|
||||
const cacheTimestamp = localStorage.getItem('menuTreeCacheTimestamp');
|
||||
const cacheVersion = localStorage.getItem('menuTreeCacheVersion');
|
||||
|
||||
// 检查缓存是否有效(24小时内)
|
||||
if (cachedMenuData && cacheTimestamp) {
|
||||
// 检查缓存是否有效(24小时内且版本匹配)
|
||||
if (cachedMenuData && cacheTimestamp && cacheVersion === 'v2') {
|
||||
const cacheAge = Date.now() - parseInt(cacheTimestamp);
|
||||
if (cacheAge < 24 * 60 * 60 * 1000) { // 24小时
|
||||
menuTree.value = JSON.parse(cachedMenuData);
|
||||
// 展开所有节点
|
||||
expandedKeys.value = getAllNodeIds(menuTree.value);
|
||||
// 强制树重渲染以应用展开
|
||||
treeKey.value++
|
||||
// 获取已保存的配置
|
||||
await loadSavedConfig();
|
||||
loading.value = false;
|
||||
@@ -325,7 +329,7 @@ const loadMenuData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await listMenu({})
|
||||
const response = await getUserMenus()
|
||||
if (response.code === 200) {
|
||||
// 过滤掉隐藏的菜单项、目录和按钮类型的菜单,只保留当前角色可访问的菜单项
|
||||
const filteredMenus = filterVisibleMenus(response.data)
|
||||
@@ -337,6 +341,10 @@ const loadMenuData = async () => {
|
||||
// 将菜单数据缓存到本地存储
|
||||
localStorage.setItem('menuTreeCache', JSON.stringify(filteredMenus));
|
||||
localStorage.setItem('menuTreeCacheTimestamp', Date.now().toString());
|
||||
localStorage.setItem('menuTreeCacheVersion', 'v2');
|
||||
|
||||
// 强制树重渲染以应用展开
|
||||
treeKey.value++
|
||||
|
||||
// 获取已保存的配置
|
||||
await loadSavedConfig()
|
||||
@@ -370,6 +378,21 @@ const filterVisibleMenus = (menus) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 程序化展开所有树节点
|
||||
const expandAllNodes = (nodes) => {
|
||||
if (!treeRef.value) return
|
||||
const store = treeRef.value.store
|
||||
const traverse = (nodeList) => {
|
||||
nodeList.forEach(node => {
|
||||
store.nodesMap[node.menuId] && (store.nodesMap[node.menuId].expanded = true)
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
}
|
||||
|
||||
// 获取所有节点ID
|
||||
const getAllNodeIds = (nodes) => {
|
||||
const ids = []
|
||||
@@ -392,69 +415,25 @@ const saveConfig = async () => {
|
||||
// 只保留菜单类型(C类型)的节点
|
||||
const validMenuNodes = checkedNodes.filter(node => node.menuType === 'C')
|
||||
|
||||
// 创建包含菜单ID和完整路径的对象数组
|
||||
const menuPromises = validMenuNodes.map(async (node) => {
|
||||
try {
|
||||
console.log(`开始获取菜单 ${node.menuName} (ID: ${node.menuId}) 的完整路径`);
|
||||
console.log(`节点对象:`, node);
|
||||
console.log(`节点的 path 属性:`, node.path);
|
||||
|
||||
// 获取菜单的完整路径
|
||||
const fullPathResponse = await getMenuFullPath(node.menuId);
|
||||
console.log(`菜单 ${node.menuName} 的完整路径响应:`, fullPathResponse);
|
||||
|
||||
let fullPath = fullPathResponse.code === 200 ? (fullPathResponse.data || fullPathResponse.msg) : node.path;
|
||||
// 确保路径格式正确,去除多余的斜杠
|
||||
if (fullPath && typeof fullPath === 'string') {
|
||||
// 将多个连续的斜杠替换为单个斜杠,但保留协议部分的双斜杠(如 http://)
|
||||
fullPath = fullPath.replace(/([^:])\/{2,}/g, '$1/');
|
||||
}
|
||||
console.log(`菜单 ${node.menuName} 的完整路径:`, fullPath);
|
||||
|
||||
const menuItem = {
|
||||
menuId: node.menuId,
|
||||
fullPath: fullPath,
|
||||
menuName: node.menuName,
|
||||
path: node.path,
|
||||
icon: node.icon, // 保存数据库中的图标类名
|
||||
menuType: node.menuType // 保存菜单类型信息
|
||||
};
|
||||
|
||||
console.log(`构造的菜单项对象:`, menuItem);
|
||||
return menuItem;
|
||||
} catch (error) {
|
||||
console.error(`获取菜单 ${node.menuName} 的完整路径失败:`, error);
|
||||
console.error(`错误堆栈:`, error.stack);
|
||||
|
||||
// 如果获取完整路径失败,使用现有路径作为备选
|
||||
console.log(`在错误处理中,节点的 path 属性:`, node.path);
|
||||
const menuItem = {
|
||||
menuId: node.menuId,
|
||||
fullPath: node.path,
|
||||
menuName: node.menuName,
|
||||
path: node.path,
|
||||
icon: node.icon, // 保存数据库中的图标类名
|
||||
menuType: node.menuType // 保存菜单类型信息
|
||||
};
|
||||
console.log(`构造的菜单项对象(错误处理):`, menuItem);
|
||||
return menuItem;
|
||||
// fullPath 已在 userMenus 接口返回时填充到节点数据上,直接使用
|
||||
const menuDataWithPaths = validMenuNodes.map((node) => {
|
||||
let fullPath = node.fullPath || node.path || ''
|
||||
// 将多个连续的斜杠替换为单个斜杠,但保留协议部分的双斜杠
|
||||
if (fullPath && typeof fullPath === 'string') {
|
||||
fullPath = fullPath.replace(/([^:])\/{2,}/g, '$1/')
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有完整路径获取完成
|
||||
const menuDataWithPaths = await Promise.all(menuPromises);
|
||||
|
||||
// 添加调试信息
|
||||
console.log('准备保存的菜单数据:', menuDataWithPaths);
|
||||
|
||||
// 检查每个对象是否包含 fullPath 属性
|
||||
menuDataWithPaths.forEach((item, index) => {
|
||||
console.log(`菜单项 ${index} 包含 fullPath:`, item.hasOwnProperty('fullPath'), '值为:', item.fullPath);
|
||||
});
|
||||
return {
|
||||
menuId: node.menuId,
|
||||
fullPath: fullPath,
|
||||
menuName: node.menuName,
|
||||
path: node.path,
|
||||
icon: node.icon,
|
||||
menuType: node.menuType
|
||||
}
|
||||
})
|
||||
|
||||
// 对配置值进行URL编码以避免特殊字符问题
|
||||
const encodedConfigValue = encodeURIComponent(JSON.stringify(menuDataWithPaths))
|
||||
console.log('编码后的配置值:', encodedConfigValue);
|
||||
|
||||
// 保存到数据库
|
||||
const saveResult = await saveCurrentUserConfig('homeFeaturesConfig', encodedConfigValue)
|
||||
|
||||
@@ -487,6 +487,33 @@ function getList() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化工具:将后端返回的日期字符串转为可读格式
|
||||
// 当时间为 00:00:00 时(历史数据 date 列迁移),只显示日期部分
|
||||
function formatDisplayDate(val) {
|
||||
if (!val) return '';
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
function formatFull(d) {
|
||||
const datePart = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
|
||||
if (h === 0 && m === 0 && s === 0) return datePart;
|
||||
return `${datePart} ${pad(h)}:${pad(m)}:${pad(s)}`;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
if (val.includes('T')) {
|
||||
const d = new Date(val);
|
||||
if (!isNaN(d.getTime())) return formatFull(d);
|
||||
}
|
||||
// yyyy-MM-dd HH:mm:ss 或 yyyy/MM/dd HH:mm:ss
|
||||
const m = val.match(/^(\d{4}[-/]\d{1,2}[-/]\d{1,2})(?:\s+(\d{1,2}:\d{2}(?::\d{2})?))?/);
|
||||
if (m) {
|
||||
if (m[2] && m[2] !== '00:00:00' && m[2] !== '00:00') return val;
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
if (val instanceof Date && !isNaN(val.getTime())) return formatFull(val);
|
||||
return val;
|
||||
}
|
||||
|
||||
// 先加载西医诊断,再加载中医诊断(避免竞态覆盖)
|
||||
getEncounterDiagnosis(props.patientInfo.encounterId).then((res) => {
|
||||
if (res.code == 200) {
|
||||
@@ -502,6 +529,7 @@ function getList() {
|
||||
syndromeDefinitionId: '',
|
||||
syndromeGroupNo: '',
|
||||
showPopover: false,
|
||||
diagnosisTime: formatDisplayDate(item.diagnosisTime),
|
||||
};
|
||||
if (obj.diagSrtNo == null) {
|
||||
obj.diagSrtNo = 1;
|
||||
@@ -540,11 +568,16 @@ function getList() {
|
||||
iptDiseTypeCode: item.iptDiseTypeCode,
|
||||
showPopover: false,
|
||||
diagnosisDoctor: item.diagnosisDoctor || props.patientInfo.practitionerName || props.patientInfo.doctorName || props.patientInfo.physicianName || userStore.name,
|
||||
diagnosisTime: item.diagnosisTime || new Date().toLocaleString('zh-CN')
|
||||
diagnosisTime: formatDisplayDate(item.diagnosisTime) || new Date().toLocaleString('zh-CN')
|
||||
});
|
||||
});
|
||||
|
||||
// 将新数据添加到现有列表现有列表
|
||||
// 先移除已有的中医诊断,防止重复追加(getList 可能被多次调用)
|
||||
form.value.diagnosisList = form.value.diagnosisList.filter(
|
||||
(item) => item.diagnosisSystem !== '中医'
|
||||
);
|
||||
|
||||
// 将新数据添加到现有列表
|
||||
form.value.diagnosisList.push(...newList);
|
||||
|
||||
// 重新排序整个列表
|
||||
@@ -899,80 +932,109 @@ function handleSaveDiagnosis() {
|
||||
item.diagSrtNo = index + 1;
|
||||
});
|
||||
|
||||
// 日期格式化工具:将 Date 对象或 ISO 字符串转为后端期望的 yyyy/M/d HH:mm:ss 格式
|
||||
function formatDateForBackend(val) {
|
||||
if (!val) return '';
|
||||
if (typeof val === 'string') {
|
||||
// 已经是字符串且符合 yyyy/M/d HH:mm:ss 格式则直接返回
|
||||
if (/^\d{4}\/\d{1,2}\/\d{1,2} \d{1,2}:\d{2}:\d{2}$/.test(val)) return val;
|
||||
// 已经是 yyyy-MM-dd HH:mm:ss 格式则转换为 yyyy/M/d HH:mm:ss
|
||||
const m = val.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2})$/);
|
||||
if (m) return `${m[1]}/${parseInt(m[2])}/${parseInt(m[3])} ${m[4]}`;
|
||||
// ISO 或其他格式则解析为 Date
|
||||
val = new Date(val);
|
||||
}
|
||||
if (val instanceof Date && !isNaN(val.getTime())) {
|
||||
const y = val.getFullYear();
|
||||
const M = val.getMonth() + 1;
|
||||
const d = val.getDate();
|
||||
const hh = String(val.getHours()).padStart(2, '0');
|
||||
const mm = String(val.getMinutes()).padStart(2, '0');
|
||||
const ss = String(val.getSeconds()).padStart(2, '0');
|
||||
return `${y}/${M}/${d} ${hh}:${mm}:${ss}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// 步骤3:拆分为西医诊断和中医诊断
|
||||
const westernList = sortedList.filter((item) => item.diagnosisSystem !== '中医');
|
||||
const tcmList = sortedList.filter((item) => item.diagnosisSystem === '中医');
|
||||
|
||||
const savePromises = [];
|
||||
// 顺序执行保存,避免并行竞态:
|
||||
// saveDoctorDiagnosis(西医)会 deleteEncounterDiagnosisInfos 删除全部诊断,
|
||||
// saveTcmDiagnosis(中医)只追加不删除,必须等西医保存完成后再保存中医
|
||||
(async () => {
|
||||
try {
|
||||
// 先保存西医诊断(会先清空再插入)
|
||||
if (westernList.length > 0) {
|
||||
await saveDiagnosis({
|
||||
patientId: props.patientInfo.patientId,
|
||||
encounterId: props.patientInfo.encounterId,
|
||||
diagnosisChildList: westernList.map(item => ({
|
||||
...item,
|
||||
diagnosisTime: formatDateForBackend(item.diagnosisTime),
|
||||
onsetDate: formatDateForBackend(item.onsetDate),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// 保存西医诊断
|
||||
if (westernList.length > 0) {
|
||||
savePromises.push(
|
||||
saveDiagnosis({
|
||||
patientId: props.patientInfo.patientId,
|
||||
encounterId: props.patientInfo.encounterId,
|
||||
diagnosisChildList: westernList,
|
||||
})
|
||||
);
|
||||
}
|
||||
// 再逐个保存中医诊断(只追加,不清空)
|
||||
for (const item of tcmList) {
|
||||
const syndromeGroupNo = item.conditionId
|
||||
? `${item.conditionId}-${item.tcmSyndromeCode || Date.now()}`
|
||||
: `${Date.now()}-${item.tcmSyndromeCode || '0'}`;
|
||||
await saveTcmDiagnosis({
|
||||
patientId: props.patientInfo.patientId,
|
||||
encounterId: props.patientInfo.encounterId,
|
||||
diagnosisChildList: [
|
||||
// 病(illness)
|
||||
{
|
||||
conditionId: item.conditionId || null,
|
||||
name: item.name,
|
||||
ybNo: item.ybNo,
|
||||
definitionId: item.definitionId || null,
|
||||
diagSrtNo: item.diagSrtNo,
|
||||
medTypeCode: item.medTypeCode,
|
||||
maindiseFlag: item.maindiseFlag,
|
||||
verificationStatusEnum: item.verificationStatusEnum,
|
||||
diagnosisDesc: item.diagnosisDesc || '',
|
||||
diagnosisDoctor: item.diagnosisDoctor || '',
|
||||
diagnosisTime: formatDateForBackend(item.diagnosisTime),
|
||||
iptDiseTypeCode: item.iptDiseTypeCode,
|
||||
syndromeGroupNo: syndromeGroupNo,
|
||||
},
|
||||
// 证(syndrome)
|
||||
{
|
||||
name: item.tcmSyndromeName,
|
||||
ybNo: item.tcmSyndromeCode,
|
||||
definitionId: item.syndromeDefinitionId || null,
|
||||
diagSrtNo: null,
|
||||
syndromeGroupNo: syndromeGroupNo,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 保存中医诊断
|
||||
tcmList.forEach((item) => {
|
||||
const syndromeGroupNo = item.conditionId
|
||||
? `${item.conditionId}-${item.tcmSyndromeCode || Date.now()}`
|
||||
: `${Date.now()}-${item.tcmSyndromeCode || '0'}`;
|
||||
savePromises.push(
|
||||
saveTcmDiagnosis({
|
||||
patientId: props.patientInfo.patientId,
|
||||
encounterId: props.patientInfo.encounterId,
|
||||
diagnosisChildList: [
|
||||
// 病(illness)
|
||||
{
|
||||
conditionId: item.conditionId || null,
|
||||
name: item.name,
|
||||
ybNo: item.ybNo,
|
||||
definitionId: item.definitionId || null,
|
||||
diagSrtNo: item.diagSrtNo,
|
||||
medTypeCode: item.medTypeCode,
|
||||
maindiseFlag: item.maindiseFlag,
|
||||
verificationStatusEnum: item.verificationStatusEnum,
|
||||
diagnosisDesc: item.diagnosisDesc || '',
|
||||
diagnosisDoctor: item.diagnosisDoctor || '',
|
||||
diagnosisTime: item.diagnosisTime || '',
|
||||
iptDiseTypeCode: item.iptDiseTypeCode,
|
||||
syndromeGroupNo: syndromeGroupNo,
|
||||
},
|
||||
// 证(syndrome)
|
||||
{
|
||||
name: item.tcmSyndromeName,
|
||||
ybNo: item.tcmSyndromeCode,
|
||||
definitionId: item.syndromeDefinitionId || null,
|
||||
diagSrtNo: null,
|
||||
syndromeGroupNo: syndromeGroupNo,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
// 所有保存完成后刷新
|
||||
emits('diagnosisSave', false);
|
||||
proxy.$modal.msgSuccess('诊断已保存');
|
||||
getList();
|
||||
|
||||
Promise.all(savePromises).then(() => {
|
||||
emits('diagnosisSave', false);
|
||||
proxy.$modal.msgSuccess('诊断已保存');
|
||||
|
||||
// 保存成功后从服务器重新加载数据,确保前后端数据一致
|
||||
getList();
|
||||
|
||||
// 食源性疾病逻辑
|
||||
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
|
||||
// 食源性疾病逻辑
|
||||
isFoodDiseasesNew({ encounterId: props.patientInfo.encounterId }).then((res2) => {
|
||||
if (res2.code === 20 && res2.data) {
|
||||
window.open(res2.data, '_blank');
|
||||
}
|
||||
});
|
||||
}).finally(() => {
|
||||
setTimeout(() => {
|
||||
isSaving.value = false;
|
||||
}, 100);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('保存诊断失败', e);
|
||||
proxy.$modal.msgError('保存诊断失败');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isSaving.value = false;
|
||||
}, 100);
|
||||
}
|
||||
})();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,9 +150,16 @@ const showApplicationFormDialog = (name) => {
|
||||
onBeforeMount(() => {});
|
||||
onMounted(() => {});
|
||||
const applicationFormNameRef = ref();
|
||||
const submitApplicationForm = () => {
|
||||
const submitApplicationForm = async () => {
|
||||
if (applicationFormNameRef.value?.submit) {
|
||||
applicationFormNameRef.value.submit();
|
||||
const success = await applicationFormNameRef.value.submit();
|
||||
if (success) {
|
||||
applicationFormDialogVisible.value = false;
|
||||
applicationFormName.value = null;
|
||||
setTimeout(() => {
|
||||
emits('refResh');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
const submitOk = () => {
|
||||
|
||||
@@ -5,7 +5,20 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="surgery-container">
|
||||
<div class="search-wrapper">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="项目代码/名称/拼音码"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
style="width: 480px;"
|
||||
@input="handleSearch"
|
||||
@clear="handleClear"
|
||||
/>
|
||||
<span class="loaded-count">已加载 {{ applicationList.length }} 项</span>
|
||||
</div>
|
||||
<div
|
||||
v-loading="loading"
|
||||
class="transfer-wrapper"
|
||||
style="min-height: 300px;"
|
||||
>
|
||||
@@ -14,10 +27,6 @@
|
||||
v-model="transferValue"
|
||||
:data="applicationList"
|
||||
:titles="['待选择', '已选择']"
|
||||
:format="leftPanelFormat"
|
||||
filterable
|
||||
filter-placeholder="项目代码/名称"
|
||||
:filter-method="filterMethod"
|
||||
/>
|
||||
</div>
|
||||
<div class="bloodTransfusion-form">
|
||||
@@ -356,12 +365,6 @@ let surgeryRecordsCache = null; // 原始 API 记录
|
||||
let surgeryMappedCache = null; // 映射后的 el-transfer 数据
|
||||
let doctorCache = null; // 医生列表(含默认主刀医生 ID)
|
||||
const transferRef = ref(null);
|
||||
const dbTotal = ref(0); // 数据库中的手术项目总数
|
||||
const checkedCount = computed(() => transferValue.value.length);
|
||||
const leftPanelFormat = computed(() => ({
|
||||
noChecked: ` 0/${dbTotal.value}`,
|
||||
hasChecked: ` ${checkedCount.value}/${dbTotal.value}`,
|
||||
}));
|
||||
// 递归查找树形科室节点
|
||||
const findTreeItem = (list, id) => {
|
||||
if (!list || list.length === 0 || id == null || id === '') return null;
|
||||
@@ -393,6 +396,23 @@ const loading = ref(false);
|
||||
const applicationList = ref([]);
|
||||
const applicationListAll = ref([]);
|
||||
const allLoading = ref(false);
|
||||
// 搜索关键字(远程搜索用)
|
||||
const searchQuery = ref('');
|
||||
let searchTimer = null;
|
||||
|
||||
/** 远程搜索:输入关键字后 300ms 防抖,调后端 API 搜索 */
|
||||
const handleSearch = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
getList(searchQuery.value || '');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
/** 清除搜索:恢复完整缓存列表 */
|
||||
const handleClear = () => {
|
||||
searchQuery.value = '';
|
||||
getList();
|
||||
};
|
||||
// 递归查找默认科室
|
||||
const findTargetDepartment = (selectProjectIds) => {
|
||||
if (!selectProjectIds || selectProjectIds.length === 0) return '';
|
||||
@@ -416,13 +436,12 @@ const getList = async (key) => {
|
||||
if (!key && surgeryRecordsCache && surgeryMappedCache) {
|
||||
applicationList.value = surgeryMappedCache;
|
||||
applicationListAll.value = surgeryRecordsCache;
|
||||
dbTotal.value = surgeryRecordsCache.length;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
return getSurgeryPage({
|
||||
pageSize: 1000,
|
||||
keyword: key || undefined,
|
||||
pageSize: key ? 500 : 100,
|
||||
searchKey: key || undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
const records = res.data.records;
|
||||
@@ -440,25 +459,15 @@ const getList = async (key) => {
|
||||
.catch((e) => {
|
||||
console.error('手术项目加载失败:', e);
|
||||
applicationList.value = [];
|
||||
dbTotal.value = 0;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* el-transfer 内置过滤方法:支持任意字符即时过滤
|
||||
* 按项目名称/代码进行前端模糊匹配
|
||||
*/
|
||||
const filterMethod = (query, item) => {
|
||||
const q = query.toLowerCase();
|
||||
const label = (item.label || '').toLowerCase();
|
||||
const key = String(item.key || '');
|
||||
return label.includes(q) || key.includes(q);
|
||||
};
|
||||
|
||||
const mapToTransferItem = (item) => ({
|
||||
key: String(item.adviceDefinitionId),
|
||||
label: `${item.adviceName} - ${item.unitCode_dictText || item.unitCode || ''}`,
|
||||
pyStr: item.pyStr || '',
|
||||
busNo: item.busNo || '',
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
@@ -605,26 +614,33 @@ watch(() => transferValue.value, (newValue) => {
|
||||
});
|
||||
const submit = () => {
|
||||
if (transferValue.value.length == 0) {
|
||||
return proxy.$message.error('请选择手术项目');
|
||||
proxy.$message.error('请选择手术项目');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (!form.targetDepartment) {
|
||||
return proxy.$message.error('请选择发往科室');
|
||||
proxy.$message.error('请选择发往科室');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// 新增必填校验
|
||||
if (!form.surgeryLevel) {
|
||||
return proxy.$message.error('请选择手术等级');
|
||||
proxy.$message.error('请选择手术等级');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (!form.anesthesiaType) {
|
||||
return proxy.$message.error('请选择麻醉方式');
|
||||
proxy.$message.error('请选择麻醉方式');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (!form.surgerySite) {
|
||||
return proxy.$message.error('请选择手术部位');
|
||||
proxy.$message.error('请选择手术部位');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (!form.mainSurgeonId) {
|
||||
return proxy.$message.error('请选择主刀医生');
|
||||
proxy.$message.error('请选择主刀医生');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (!form.plannedTime) {
|
||||
return proxy.$message.error('请选择预定手术时间');
|
||||
proxy.$message.error('请选择预定手术时间');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
let applicationListAllFilter = applicationListAll.value.filter((item) => {
|
||||
return transferValue.value.includes(item.adviceDefinitionId);
|
||||
@@ -651,7 +667,7 @@ const submit = () => {
|
||||
const assistant2Doc = doctorOptions.value.find(d => d.id === form.assistant2Id);
|
||||
form.assistant2Name = assistant2Doc ? assistant2Doc.name : '';
|
||||
|
||||
saveSurgery({
|
||||
return saveSurgery({
|
||||
activityList: applicationListAllFilter,
|
||||
patientId: patientInfo.value.patientId, //患者ID
|
||||
encounterId: patientInfo.value.encounterId, // 就诊ID
|
||||
@@ -666,9 +682,14 @@ const submit = () => {
|
||||
applicationList.value = [];
|
||||
editingRequestFormId.value = '';
|
||||
emits('submitOk');
|
||||
return true;
|
||||
} else {
|
||||
proxy.$message.error(res.message);
|
||||
return false;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('手术申请保存失败:', err);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
/** 递归规范化树形科室 ID 为字符串,确保与 el-tree-select / findTreeItem 兼容 */
|
||||
@@ -727,6 +748,19 @@ defineExpose({ state, submit, fillForm, getLocationInfo, getDiagnosisList, getLi
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.loaded-count {
|
||||
font-size: 13px;
|
||||
color: #64748B;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-wrapper {
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
>
|
||||
<el-select
|
||||
v-model="form.targetLocationId"
|
||||
no-data-text="请先选择科室"
|
||||
:no-data-text="form.targetOrganizationId ? '该科室暂无对应病区' : '请先选择科室'"
|
||||
placeholder="请选择转入病区"
|
||||
>
|
||||
<el-option
|
||||
@@ -70,7 +70,7 @@
|
||||
<el-input
|
||||
v-model="form.reasonText"
|
||||
type="textarea"
|
||||
rows="5"
|
||||
:rows="5"
|
||||
placeholder="请输入转科原因"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="inpatientDoctor-order-container" style="width: 100%">
|
||||
<div style="margin-bottom: 5px" class="order-operate-btn">
|
||||
<div style="height: 44px; display: flex; align-items: center; flex: none">
|
||||
@@ -216,10 +216,14 @@
|
||||
/>
|
||||
</el-select>
|
||||
<el-popover
|
||||
:popper-style="{ padding: '0' }"
|
||||
:popper-style="advicePopperStyle"
|
||||
placement="bottom-start"
|
||||
popper-class="order-advice-popper"
|
||||
:offset="0"
|
||||
:visible="scope.row.showPopover"
|
||||
:width="1200"
|
||||
:teleported="true"
|
||||
:popper-options="advicePopperOptions"
|
||||
>
|
||||
<adviceBaseList
|
||||
ref="adviceTableRef"
|
||||
@@ -232,7 +236,7 @@
|
||||
style="width: 62%"
|
||||
v-model="scope.row.adviceName"
|
||||
placeholder="请选择项目"
|
||||
@input="handleChange"
|
||||
@input="(value) => handleChange(value, scope.row, scope.rowIndex)"
|
||||
@focus="handleFocus(scope.row, scope.rowIndex)"
|
||||
@keyup.enter.stop="handleFocus(scope.row, scope.rowIndex)"
|
||||
@keydown="
|
||||
@@ -255,9 +259,11 @@
|
||||
<span v-else>{{ scope.row.adviceName }}</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="状态" align="center" field="" width="90">
|
||||
<vxe-column title="状态" align="center" field="" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.chargeStatus == 5" type="info">
|
||||
<el-tag v-if="scope.row.statusEnum == 6" type="danger">停止</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 13" type="warning">已停嘱(待核对)</el-tag>
|
||||
<el-tag v-else-if="scope.row.chargeStatus == 5" type="info">
|
||||
{{ scope.row.chargeStatus_enumText }}
|
||||
</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 2" type="success">已签发</el-tag>
|
||||
@@ -268,7 +274,6 @@
|
||||
<el-tag v-else-if="scope.row.statusEnum == 10" type="primary">已校对</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 11" type="primary">待接收</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 3" type="success">已校对</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 6" type="danger">停止</el-tag>
|
||||
<el-tag v-else-if="scope.row.statusEnum == 20" type="success">已完成</el-tag>
|
||||
<el-tag v-else type="info">{{ scope.row.chargeStatus_enumText }}</el-tag>
|
||||
</template>
|
||||
@@ -307,15 +312,17 @@
|
||||
</vxe-column>
|
||||
<vxe-column title="频次/用法" align="center" field="" width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="!scope.row.isEdit && scope.row.adviceType == 1" style="text-align: right">
|
||||
<span v-if="!scope.row.isEdit && (scope.row.adviceType == 1 || scope.row.adviceType == 7 || scope.row.adviceType == 8)" style="text-align: right">
|
||||
{{
|
||||
[
|
||||
scope.row.rateCode_dictText,
|
||||
scope.row.dispensePerDuration ? scope.row.dispensePerDuration + '天' : '',
|
||||
scope.row.methodCode_dictText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
scope.row.adviceType == 8
|
||||
? (scope.row.rateCode_dictText || scope.row.rateCode || '-')
|
||||
: [
|
||||
scope.row.rateCode_dictText,
|
||||
scope.row.dispensePerDuration ? scope.row.dispensePerDuration + '天' : '',
|
||||
scope.row.methodCode_dictText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
@@ -495,6 +502,24 @@ const loading = ref(false);
|
||||
// Bug #587: 标记弹窗刚被关闭的行uniqueKey,防止关闭弹窗时误触发行展开
|
||||
const popoverJustClosedByKey = ref(null);
|
||||
|
||||
// 医嘱检索下拉浮框对齐:跟踪表格水平滚动偏移与主体区域边界限制
|
||||
const tableScrollLeft = ref(0);
|
||||
const mainBoundary = ref(null);
|
||||
const advicePopperStyle = computed(() => ({
|
||||
padding: '0',
|
||||
marginLeft: `-${tableScrollLeft.value}px`,
|
||||
}));
|
||||
const advicePopperOptions = computed(() => ({
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: mainBoundary.value || 'viewport',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// 停嘱弹窗
|
||||
const stopDialogVisible = ref(false);
|
||||
const stopForm = reactive({
|
||||
@@ -588,8 +613,9 @@ const adviceTypeList = computed(() => {
|
||||
hasShownPharmacyConfigWarning.value = true;
|
||||
}
|
||||
|
||||
// 只返回不需要取药科室配置的类别(诊疗、手术、出院带药)
|
||||
// 只返回不需要取药科室配置的类别(诊疗、耗材、手术、出院带药)
|
||||
return [
|
||||
{ label: '耗材', value: 2, adviceType: 2, categoryCode: '' },
|
||||
{ label: '诊疗', value: 3, adviceType: 3, categoryCode: '' },
|
||||
{ label: '手术', value: 6, adviceType: 6, categoryCode: '' },
|
||||
{ label: '出院带药', value: 7, adviceType: 7, categoryCode: '' },
|
||||
@@ -617,7 +643,8 @@ const adviceTypeList = computed(() => {
|
||||
typeList.push({ label: '中草药', value: '1-4', adviceType: 1, categoryCode: '4' });
|
||||
}
|
||||
|
||||
// 始终添加诊疗和手术(它们不受取药配置限制)
|
||||
// 始终添加诊疗、耗材和手术(它们不受取药配置限制)
|
||||
typeList.push({ label: '耗材', value: 2, adviceType: 2, categoryCode: '' });
|
||||
typeList.push({ label: '诊疗', value: 3, adviceType: 3, categoryCode: '' });
|
||||
typeList.push({ label: '手术', value: 6, adviceType: 6, categoryCode: '' });
|
||||
|
||||
@@ -642,6 +669,10 @@ const statusOption = [
|
||||
label: '已签发',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '已停嘱',
|
||||
value: 13,
|
||||
},
|
||||
{
|
||||
label: '停止',
|
||||
value: 6,
|
||||
@@ -655,11 +686,28 @@ const statusOption = [
|
||||
// loadingInstance removed - using loading ref instead
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', escKeyListener);
|
||||
// 监听表格水平滚动,同步更新医嘱检索下拉浮框的水平偏移
|
||||
nextTick(() => {
|
||||
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
|
||||
if (scrollWrapper) {
|
||||
scrollWrapper.addEventListener('scroll', onTableScroll, { passive: true });
|
||||
}
|
||||
// 获取主体区域容器作为 Popper 边界,防止浮框向左移入患者列表
|
||||
mainBoundary.value = document.querySelector('.inpatientDoctor-home-main');
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', escKeyListener);
|
||||
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
|
||||
if (scrollWrapper) {
|
||||
scrollWrapper.removeEventListener('scroll', onTableScroll);
|
||||
}
|
||||
});
|
||||
|
||||
function onTableScroll(e) {
|
||||
tableScrollLeft.value = e.target.scrollLeft || 0;
|
||||
}
|
||||
watch(
|
||||
() => expandOrder.value,
|
||||
(newValue) => {
|
||||
@@ -758,6 +806,9 @@ function getListInfo(addNewRow) {
|
||||
// 🔧 修复:同时保存 orgName,当 orgId 在科室树中匹配不到时作为兜底显示
|
||||
// 优先从科室树查找名称,其次用 positionName(后端已保存的科室名),最后用 contentJson 中的 orgName
|
||||
orgName: findOrgName(item.positionId || parsedContent?.orgId || item.orgId) || item.positionName || parsedContent?.orgName || undefined,
|
||||
// 确保文字医嘱的 rateCode / rateCode_dictText 不被 item 中的 null/undefined 覆盖
|
||||
rateCode: item.rateCode || parsedContent?.rateCode || undefined,
|
||||
rateCode_dictText: item.rateCode_dictText || parsedContent?.rateCode_dictText || undefined,
|
||||
// Bug #589: 从contentJson检测出院带药标记,恢复类型显示
|
||||
// 后端存储时adviceType转为1(药品),通过prescriptionCategory=3标识出院带药
|
||||
...(parsedContent?.prescriptionCategory == 3 ? {
|
||||
@@ -919,6 +970,8 @@ function handleAddPrescription() {
|
||||
statusEnum: 1,
|
||||
therapyEnum: '2', // 默认为临时医嘱
|
||||
startTime: defaultStartTime,
|
||||
requesterId_dictText: userStore.nickName,
|
||||
requesterId: userStore.id,
|
||||
});
|
||||
getGroupMarkers();
|
||||
nextTick(() => {
|
||||
@@ -991,6 +1044,13 @@ function clickRowDb({ row, column, event }) {
|
||||
if (row.statusEnum == 1) {
|
||||
// 确保治疗类型为字符串,方便与单选框 label 对齐,默认为长期医嘱('1')
|
||||
row.therapyEnum = String(row.therapyEnum ?? '1');
|
||||
if (row.adviceType == 8) {
|
||||
if (!row.orgId && userStore.orgId) {
|
||||
row.orgId = userStore.orgId;
|
||||
row.positionId = userStore.orgId;
|
||||
row.orgName = userStore.orgName || findOrgName(userStore.orgId) || '';
|
||||
}
|
||||
}
|
||||
row.isEdit = true;
|
||||
const index = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey);
|
||||
rowIndex.value = index;
|
||||
@@ -1073,6 +1133,14 @@ function handleDiagnosisChange(item) {
|
||||
function expandTextRow(rowIndex) {
|
||||
const row = filterPrescriptionList.value[rowIndex];
|
||||
if (!row) return;
|
||||
|
||||
// 自动获取当前用户所在的科室
|
||||
if (!row.orgId && userStore.orgId) {
|
||||
row.orgId = userStore.orgId;
|
||||
row.positionId = userStore.orgId;
|
||||
row.orgName = userStore.orgName || findOrgName(userStore.orgId) || '';
|
||||
}
|
||||
|
||||
expandOrder.value = [row.uniqueKey];
|
||||
nextTick(() => {
|
||||
if (prescriptionRef.value?.setRowExpand) {
|
||||
@@ -1082,7 +1150,17 @@ function expandTextRow(rowIndex) {
|
||||
}
|
||||
|
||||
function handleFocus(row, index) {
|
||||
rowIndex.value = index;
|
||||
const actualIndex = prescriptionList.value.findIndex((item) => item.uniqueKey === row.uniqueKey);
|
||||
if (actualIndex !== -1) {
|
||||
rowIndex.value = actualIndex;
|
||||
} else {
|
||||
rowIndex.value = index;
|
||||
}
|
||||
// 同步表格水平滚动偏移,确保浮框位置正确
|
||||
const scrollWrapper = document.querySelector('.vxe-table--body-wrapper');
|
||||
if (scrollWrapper) {
|
||||
tableScrollLeft.value = scrollWrapper.scrollLeft || 0;
|
||||
}
|
||||
// 文字医嘱(type=8)不弹药品搜索框,直接展开填写面板
|
||||
const adviceType = row.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
|
||||
if (adviceType == 8) {
|
||||
@@ -1111,11 +1189,9 @@ function handleBlur(row) {
|
||||
popoverJustClosedByKey.value = row.uniqueKey;
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
function handleChange(value, row, index) {
|
||||
// 文字医嘱(type=8)不触发药品搜索
|
||||
const currentIndex = rowIndex.value;
|
||||
if (currentIndex < 0) return;
|
||||
const row = filterPrescriptionList.value[currentIndex];
|
||||
if (index < 0 || !row) return;
|
||||
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
|
||||
if (adviceType == 8) {
|
||||
return;
|
||||
@@ -1126,7 +1202,7 @@ function handleChange(value) {
|
||||
if (!row.showPopover) {
|
||||
row.showPopover = true;
|
||||
}
|
||||
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[currentIndex] : adviceTableRef.value;
|
||||
const tableRef = Array.isArray(adviceTableRef.value) ? adviceTableRef.value[index] : adviceTableRef.value;
|
||||
if (tableRef && tableRef.refresh) {
|
||||
const adviceType = row?.adviceType !== undefined ? row.adviceType : adviceQueryParams.value.adviceType;
|
||||
let categoryCode = '';
|
||||
@@ -1144,8 +1220,14 @@ function handleChange(value) {
|
||||
*/
|
||||
function selectAdviceBase(key, row) {
|
||||
|
||||
// 每次选择药品时,根据 uniqueKey 找到 prescriptionList 中的正确索引
|
||||
const actualIndex = prescriptionList.value.findIndex((item) => item.uniqueKey === key);
|
||||
if (actualIndex !== -1) {
|
||||
rowIndex.value = actualIndex;
|
||||
}
|
||||
|
||||
// 每次选择药品时,将当前行数据初始化为最初状态
|
||||
const currentUniqueKey = prescriptionList.value[rowIndex.value]?.uniqueKey || key;
|
||||
const currentUniqueKey = key;
|
||||
const prevRow = prescriptionList.value[rowIndex.value] || {};
|
||||
prescriptionList.value[rowIndex.value] = {
|
||||
uniqueKey: currentUniqueKey,
|
||||
@@ -1157,6 +1239,8 @@ function selectAdviceBase(key, row) {
|
||||
// Bug #589: 出院带药需要保留类型和标志,setValue中可能被API数据覆盖
|
||||
adviceType: prevRow.adviceType || undefined,
|
||||
dischargeFlag: prevRow.dischargeFlag || undefined,
|
||||
requesterId_dictText: userStore.nickName,
|
||||
requesterId: userStore.id,
|
||||
};
|
||||
try {
|
||||
setValue(row);
|
||||
@@ -1336,7 +1420,7 @@ function handleDelete() {
|
||||
deleteList.push({
|
||||
requestId: deleteItem.requestId,
|
||||
dbOpType: '3',
|
||||
adviceType: deleteItem.adviceType,
|
||||
adviceType: deleteItem.adviceType == 7 ? 1 : deleteItem.adviceType,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1650,6 +1734,8 @@ function handleSaveSign(row, index) {
|
||||
}
|
||||
|
||||
// 更新UI状态
|
||||
row.requesterId_dictText = userStore.nickName;
|
||||
row.requesterId = userStore.id;
|
||||
row.isEdit = false;
|
||||
isAdding.value = false;
|
||||
collapseAllExpanded();
|
||||
@@ -1899,6 +1985,7 @@ function setValue(row) {
|
||||
...baseRow,
|
||||
uniqueKey: currentUniqueKey, // 确保 uniqueKey 不被覆盖
|
||||
// Bug #589: 出院带药在 baseRow 中被药品的 adviceType=1 覆盖,此处恢复
|
||||
dischargeFlag: prevRow.dischargeFlag, // 显式保留,防止被 baseRow 中的 undefined/null/0 覆盖
|
||||
adviceType: prevRow.dischargeFlag ? 7 : baseRow.adviceType,
|
||||
// 🔧 修复执行科室逻辑:
|
||||
// 1. 非诊疗类型(adviceType!=3)不需要执行科室,设为undefined
|
||||
@@ -2011,6 +2098,8 @@ function handleSaveGroup(orderGroupList) {
|
||||
// 创建新的处方项目
|
||||
const newRow = {
|
||||
...prescriptionList.value[tempIndex],
|
||||
requesterId_dictText: userStore.nickName,
|
||||
requesterId: userStore.id,
|
||||
patientId: patientInfo.value.patientId,
|
||||
encounterId: patientInfo.value.encounterId,
|
||||
accountId: accountId.value,
|
||||
@@ -2070,6 +2159,8 @@ function handleSaveGroup(orderGroupList) {
|
||||
function handleSaveHistory(value) {
|
||||
let saveRow = {
|
||||
...value,
|
||||
requesterId_dictText: userStore.nickName,
|
||||
requesterId: userStore.id,
|
||||
patientId: patientInfo.value.patientId,
|
||||
encounterId: patientInfo.value.encounterId,
|
||||
accountId: accountId.value,
|
||||
@@ -2236,7 +2327,7 @@ function handleStopAdvice() {
|
||||
// 找出停嘱的
|
||||
for (let index = 0; index < selectRows.length; index++) {
|
||||
const item = selectRows[index];
|
||||
if (item.statusEnum == 6) {
|
||||
if (item.statusEnum == 6 || item.statusEnum == 13) {
|
||||
isStop = false;
|
||||
break;
|
||||
}
|
||||
@@ -2974,4 +3065,9 @@ defineExpose({ getListInfo, getDiagnosisInfo });
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 医嘱检索下拉浮框:teleported 到 body,需用 :global 选择
|
||||
:global(.order-advice-popper) {
|
||||
transition: margin-left 0.05s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
field="requestTime"
|
||||
width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
<template #default>
|
||||
{{ '1支' }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
@@ -166,17 +166,17 @@
|
||||
>
|
||||
<template #default="scope">
|
||||
<div
|
||||
v-for="(item, timeIndex) in scope.row.times"
|
||||
v-for="(timeItem, timeIndex) in scope.row.times"
|
||||
:key="timeIndex"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<span>{{ item }}</span>
|
||||
<span>{{ timeItem }}</span>
|
||||
<el-checkbox-group
|
||||
v-model="scope.row.checkedRates[item]"
|
||||
v-model="scope.row.checkedRates[timeItem]"
|
||||
style="display: inline-block; margin-left: 15px"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(rateItem, rateIndex) in scope.row.rate[item]"
|
||||
v-for="(rateItem, rateIndex) in scope.row.rate[timeItem]"
|
||||
:key="rateIndex"
|
||||
:value="rateItem.rate"
|
||||
border
|
||||
@@ -252,7 +252,6 @@ const activeNames = ref([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const prescriptionList = ref([]);
|
||||
const deadline = ref(formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59');
|
||||
const { proxy } = getCurrentInstance();
|
||||
const loading = ref(false);
|
||||
const chooseAll = ref(false);
|
||||
@@ -267,6 +266,7 @@ const props = defineProps({
|
||||
},
|
||||
deadline: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
therapyEnum: {
|
||||
type: Number,
|
||||
@@ -309,8 +309,8 @@ function handleGetPrescription() {
|
||||
});
|
||||
|
||||
// 将全部的时间点拆分 把日期去重,页面显示示例 05-15 [01:30 02:30 03:30]
|
||||
let time = item.dispenseTime?.substring(5, 10);
|
||||
let rateTime = item.dispenseTime?.substring(11, 16);
|
||||
let time = item.executeTime?.substring(5, 10);
|
||||
let rateTime = item.executeTime?.substring(11, 16);
|
||||
times.add(time);
|
||||
rate[time] = rate[time] || [];
|
||||
rate[time].push({ rate: rateTime, dispenseId: item.dispenseId });
|
||||
@@ -367,7 +367,7 @@ function handleMedicineSummary() {
|
||||
const now = proxy.formatDateStr(new Date(), 'YYYY-MM-DD HH:mm:ss');
|
||||
paramList.forEach((item) => {
|
||||
item.dispenseIds.forEach((d) => {
|
||||
ids.push({ ...d, dispenseTime: now });
|
||||
ids.push({ ...d, executeTime: now });
|
||||
});
|
||||
});
|
||||
if (ids.length === 0) {
|
||||
|
||||
@@ -273,7 +273,15 @@
|
||||
</vxe-column>
|
||||
<vxe-column title="医嘱状态" align="center" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.requestStatus_enumText || '-' }}
|
||||
<el-tag v-if="scope.row.requestStatus == 6" type="danger">停止</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 13" type="warning">已停嘱(待核对)</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 2" type="success">已签发</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 1" type="primary">待签发</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 10" type="primary">已校对</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 11" type="primary">待接收</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 3" type="success">已校对</el-tag>
|
||||
<el-tag v-else-if="scope.row.requestStatus == 20" type="success">已完成</el-tag>
|
||||
<el-tag v-else type="info">{{ scope.row.requestStatus_enumText || '-' }}</el-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="执行科室" align="center" min-width="120">
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
<vxe-column
|
||||
title="医嘱状态"
|
||||
field="requestStatus_enumText"
|
||||
width="100"
|
||||
width="110"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
@@ -235,19 +235,39 @@
|
||||
field="singleDose"
|
||||
width="100"
|
||||
align="center"
|
||||
/>
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.singleDose ? scope.row.singleDose + ' ' + (scope.row.doseUnitCode_dictText || '') : '' }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="总量"
|
||||
field="totalAmount"
|
||||
width="100"
|
||||
align="center"
|
||||
/>
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.totalAmount }} {{ scope.row.unitCode_dictText || '' }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="频次/用法"
|
||||
field="frequencyUsage"
|
||||
width="110"
|
||||
width="130"
|
||||
align="center"
|
||||
/>
|
||||
>
|
||||
<template #default="scope">
|
||||
{{
|
||||
[
|
||||
scope.row.rateCode_dictText,
|
||||
scope.row.dispensePerDuration ? scope.row.dispensePerDuration + '天' : '',
|
||||
scope.row.methodCode_dictText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="皮试"
|
||||
field="skinTestStatus"
|
||||
@@ -283,10 +303,14 @@
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="开嘱医生"
|
||||
field="orderingDoctor"
|
||||
field="requesterId_dictText"
|
||||
width="100"
|
||||
align="center"
|
||||
/>
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.requesterId_dictText || scope.row.orderingDoctor || '-' }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="退回原因"
|
||||
field="reasonText"
|
||||
@@ -372,6 +396,7 @@ const REQUEST_STATUS_DISPLAY = {
|
||||
[RequestStatus.ACTIVE]: '已签发',
|
||||
[RequestStatus.COMPLETED]: '已校对',
|
||||
[RequestStatus.STOPPED]: '已停止',
|
||||
[RequestStatus.PENDING_STOP]: '已停嘱',
|
||||
};
|
||||
|
||||
/** 发药状态 → 医嘱状态映射表 */
|
||||
@@ -395,17 +420,21 @@ const LEGACY_STATUS_TEXT = {
|
||||
};
|
||||
|
||||
const getStatusDisplayText = (row) => {
|
||||
// 1. 优先使用发药状态
|
||||
// 1. 优先使用行级别请求状态:如果已停止或停嘱中,直接显示该状态
|
||||
const requestCode = Number(row?.requestStatus);
|
||||
if (requestCode === RequestStatus.STOPPED || requestCode === RequestStatus.PENDING_STOP) {
|
||||
return REQUEST_STATUS_DISPLAY[requestCode];
|
||||
}
|
||||
// 2. 其次使用发药状态
|
||||
const dispenseCode = Number(row?.dispenseStatus);
|
||||
if (DISPENSE_STATUS_DISPLAY[dispenseCode]) {
|
||||
return DISPENSE_STATUS_DISPLAY[dispenseCode];
|
||||
}
|
||||
// 2. 使用行级别请求状态
|
||||
const requestCode = Number(row?.requestStatus);
|
||||
// 3. 最后回退到其他请求状态
|
||||
if (REQUEST_STATUS_DISPLAY[requestCode]) {
|
||||
return REQUEST_STATUS_DISPLAY[requestCode];
|
||||
}
|
||||
// 3. 兼容旧后端枚举文本(如"已发送"→"已签发")
|
||||
// 4. 兼容旧后端枚举文本(如"已发送"→"已签发")
|
||||
return LEGACY_STATUS_TEXT[row?.requestStatus_enumText] || row?.requestStatus_enumText || '';
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>未闭环医嘱预警</span>
|
||||
<el-tag type="danger" size="small">{{ unclosedWarnings.length }} 条待处理</el-tag>
|
||||
<el-tag type="danger" size="small">{{ warningTotal }} 条待处理</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-loading="warningLoading" :data="unclosedWarnings" border>
|
||||
@@ -97,6 +97,16 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="开具时间" align="center" prop="orderTime" width="180" />
|
||||
</el-table>
|
||||
<el-pagination
|
||||
style="margin-top:16px;justify-content:flex-end"
|
||||
v-model:current-page="warningPage"
|
||||
v-model:page-size="warningPageSize"
|
||||
:total="warningTotal"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadWarnings"
|
||||
@current-change="loadWarnings"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -167,25 +177,25 @@ function loadWarnings() {
|
||||
}
|
||||
|
||||
function handleRemind(row) {
|
||||
ElMessageBox.confirm('??? ' + row.doctorName + ' ???????', '????', {
|
||||
ElMessageBox.confirm('确认提醒 ' + row.doctorName + ' 处理未闭环医嘱?', '催办提醒', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '????',
|
||||
cancelButtonText: '??'
|
||||
confirmButtonText: '确认催办',
|
||||
cancelButtonText: '取消'
|
||||
}).then(() => {
|
||||
apiRemindOrder({ orderNo: row.orderNo, message: '?????????????' }).then(res => {
|
||||
apiRemindOrder({ orderNo: row.orderNo, message: '您有未闭环医嘱需要处理' }).then(res => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('???????? ' + row.doctorName)
|
||||
ElMessage.success('催办消息已发送给 ' + row.doctorName)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '????')
|
||||
ElMessage.error(res.msg || '催办失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage.error('??????')
|
||||
ElMessage.error('催办失败')
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleViewDetail(row) {
|
||||
ElMessage.info('???: ' + row.orderNo + ' | ??: ' + row.patientName + ' | ????: ' + row.currentStep)
|
||||
ElMessage.info('医嘱号: ' + row.orderNo + ' | 患者: ' + row.patientName + ' | 当前环节: ' + row.currentStep)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<vxe-table
|
||||
:row-config="{ isCurrent: true }" :data="filteredCandidatePoolList"
|
||||
:data="filteredCandidatePoolList"
|
||||
stripe
|
||||
border
|
||||
height="100%"
|
||||
@@ -687,15 +687,15 @@ import {
|
||||
} from '../api'
|
||||
|
||||
// 当前日期 & 统计信息(总已签到/在队列中)
|
||||
const currentDate = ref('2025/12/22 上午')
|
||||
const currentDate = ref('')
|
||||
const totalSignedIn = ref(0)
|
||||
const totalInQueue = ref(0)
|
||||
|
||||
// 当前呼叫信息
|
||||
const currentCall = ref({
|
||||
number: '1',
|
||||
name: '郑华',
|
||||
room: '4号诊室'
|
||||
number: '-',
|
||||
name: '-',
|
||||
room: '-'
|
||||
})
|
||||
|
||||
// 当前选中的队列行(用于选呼)
|
||||
@@ -785,85 +785,6 @@ const weekOptions = [
|
||||
// 叫号类型
|
||||
const callType = ref('选呼')
|
||||
|
||||
// ============ 数据初始化/刷新(前端模拟) ============
|
||||
const getInitialCandidatePoolList = () => ([
|
||||
{
|
||||
sequenceNo: 12,
|
||||
patientName: '陈明',
|
||||
age: 65,
|
||||
appointmentType: '专家',
|
||||
room: '3号诊室',
|
||||
doctor: '张医生',
|
||||
matchingRule: '年龄≥60'
|
||||
},
|
||||
{
|
||||
sequenceNo: 13,
|
||||
patientName: '刘芳',
|
||||
age: 58,
|
||||
appointmentType: '普通',
|
||||
room: '4号诊室',
|
||||
doctor: '李医生',
|
||||
matchingRule: '-'
|
||||
},
|
||||
{
|
||||
sequenceNo: 14,
|
||||
patientName: '周强',
|
||||
age: 45,
|
||||
appointmentType: '普通',
|
||||
room: '5号诊室',
|
||||
doctor: '王医生',
|
||||
matchingRule: '-'
|
||||
},
|
||||
{
|
||||
sequenceNo: 15,
|
||||
patientName: '吴伟',
|
||||
age: 72,
|
||||
appointmentType: '专家',
|
||||
room: '3号诊室',
|
||||
doctor: '张医生',
|
||||
matchingRule: '年龄≥70'
|
||||
}
|
||||
])
|
||||
|
||||
const getInitialQueueList = () => ([
|
||||
{
|
||||
queueOrder: 1,
|
||||
patientName: '林静',
|
||||
appointmentType: '专家',
|
||||
room: '3号诊室',
|
||||
doctor: '张医生',
|
||||
waitingTime: '05:00',
|
||||
status: '等待'
|
||||
},
|
||||
{
|
||||
queueOrder: 2,
|
||||
patientName: '郑华',
|
||||
appointmentType: '普通',
|
||||
room: '4号诊室',
|
||||
doctor: '李医生',
|
||||
waitingTime: '00:00',
|
||||
status: '叫号中'
|
||||
},
|
||||
{
|
||||
queueOrder: 3,
|
||||
patientName: '王丽',
|
||||
appointmentType: '普通',
|
||||
room: '5号诊室',
|
||||
doctor: '王医生',
|
||||
waitingTime: '08:00',
|
||||
status: '等待'
|
||||
},
|
||||
{
|
||||
queueOrder: 4,
|
||||
patientName: '张伟',
|
||||
appointmentType: '专家',
|
||||
room: '3号诊室',
|
||||
doctor: '张医生',
|
||||
waitingTime: '12:00',
|
||||
status: '等待'
|
||||
}
|
||||
])
|
||||
|
||||
const syncCurrentCallFromQueue = () => {
|
||||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||||
if (!calling) {
|
||||
@@ -881,17 +802,6 @@ const syncCurrentCallFromQueue = () => {
|
||||
}
|
||||
|
||||
// ============ 与后端接口联动 ============
|
||||
// 注意:后端未实现或出错时会自动退回本地假数据
|
||||
// 将分钟数转换为 MM:SS 格式
|
||||
const formatMinutesToMmSs = (minutes) => {
|
||||
if (minutes == null || minutes < 0) return '00:00'
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
const hoursStr = String(hours).padStart(2, '0')
|
||||
const minsStr = String(mins).padStart(2, '0')
|
||||
return `${hoursStr}:${minsStr}`
|
||||
}
|
||||
|
||||
// 将年龄字符串转换为数字(处理 "20岁"、"1小时" 等格式)
|
||||
const parseAge = (ageStr) => {
|
||||
if (!ageStr) return 0
|
||||
@@ -1023,20 +933,6 @@ const loadQueueFromDb = async (dateStr) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态枚举文本转换为前端状态
|
||||
const mapStatusToFrontend = (statusEnumText) => {
|
||||
if (!statusEnumText) return '等待'
|
||||
// 状态映射:就诊中 -> 叫号中,其他 -> 等待
|
||||
if (statusEnumText.includes('就诊中') || statusEnumText.includes('在诊')) {
|
||||
return '叫号中'
|
||||
} else if (statusEnumText.includes('已完成') || statusEnumText.includes('已出院')) {
|
||||
return '已完成'
|
||||
} else if (statusEnumText.includes('已取消') || statusEnumText.includes('退号')) {
|
||||
return '已取消'
|
||||
}
|
||||
return '等待'
|
||||
}
|
||||
|
||||
// 判断日期是否是今天
|
||||
const isToday = (date) => {
|
||||
if (!date) return false
|
||||
@@ -1056,7 +952,7 @@ const filterTodayData = (data) => {
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
|
||||
return data.filter(item => {
|
||||
if (!item.registerTime) return false
|
||||
if (!item.registerTime) return true // 无挂号时间的保留(如当天挂号未设置时间)
|
||||
const registerDate = new Date(item.registerTime)
|
||||
return registerDate >= today && registerDate < tomorrow
|
||||
})
|
||||
@@ -1108,12 +1004,12 @@ const loadDataFromApi = async () => {
|
||||
}))
|
||||
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
|
||||
} else {
|
||||
console.log('【心内科】候选池数据为空数组或非数组,使用默认数据')
|
||||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||||
console.log('【心内科】候选池数据为空数组或非数组')
|
||||
originalCandidatePoolList.value = []
|
||||
}
|
||||
} else {
|
||||
console.log('【心内科】候选池响应为空或格式错误,使用默认数据')
|
||||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||||
console.log('【心内科】候选池响应为空或格式错误')
|
||||
originalCandidatePoolList.value = []
|
||||
}
|
||||
|
||||
// 2) 队列列表:从数据库读取(可刷新、可恢复)
|
||||
@@ -1161,10 +1057,8 @@ const loadDataFromApi = async () => {
|
||||
console.log('【心内科】数据加载完成:候选池', originalCandidatePoolList.value.length, '条,队列', originalQueueList.value.length, '条')
|
||||
ElMessage.success('【心内科】已从门诊挂号接口加载数据')
|
||||
} catch (e) {
|
||||
console.error('【心内科】loadDataFromApi 执行异常,使用本地假数据:', e)
|
||||
// 任何异常:回退本地假数据
|
||||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||||
// 队列不再回退假数据,避免误导
|
||||
console.error('【心内科】loadDataFromApi 执行异常:', e)
|
||||
originalCandidatePoolList.value = []
|
||||
originalQueueList.value = []
|
||||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||||
totalInQueue.value = originalQueueList.value.length
|
||||
@@ -1173,7 +1067,7 @@ const loadDataFromApi = async () => {
|
||||
}
|
||||
|
||||
// 原始数据存储(用于过滤)
|
||||
const originalCandidatePoolList = ref(getInitialCandidatePoolList())
|
||||
const originalCandidatePoolList = ref([])
|
||||
|
||||
// 过滤后的智能候选池数据(按诊室过滤)
|
||||
const filteredCandidatePoolList = computed(() => {
|
||||
@@ -1184,7 +1078,7 @@ const filteredCandidatePoolList = computed(() => {
|
||||
})
|
||||
|
||||
// 原始队列数据存储(用于过滤)
|
||||
const originalQueueList = ref(getInitialQueueList())
|
||||
const originalQueueList = ref([])
|
||||
|
||||
// 动态计算已加载数据中的唯一诊室列表(依赖上方两个 ref,确保声明顺序正确)
|
||||
const uniqueRooms = computed(() => {
|
||||
@@ -1231,27 +1125,13 @@ const filteredQueueList = computed(() => {
|
||||
return filtered
|
||||
})
|
||||
|
||||
const handleQueueRowClick = (row) => {
|
||||
const handleQueueRowClick = ({ row }) => {
|
||||
selectedQueueRow.value = row
|
||||
}
|
||||
|
||||
// 候选池选择变化
|
||||
const handleCandidateSelectionChange = (selection) => {
|
||||
selectedCandidates.value = selection
|
||||
}
|
||||
|
||||
// 获取下一个队序号
|
||||
const getNextQueueOrder = () => {
|
||||
if (originalQueueList.value.length === 0) return 1
|
||||
const maxOrder = Math.max(...originalQueueList.value.map(item => item.queueOrder))
|
||||
return maxOrder + 1
|
||||
}
|
||||
|
||||
// 重新计算队序号(保持连续)
|
||||
const recalculateQueueOrders = () => {
|
||||
originalQueueList.value.forEach((item, index) => {
|
||||
item.queueOrder = index + 1
|
||||
})
|
||||
const handleCandidateSelectionChange = ({ records }) => {
|
||||
selectedCandidates.value = records
|
||||
}
|
||||
|
||||
// 加入队列(选中的候选池患者)——落库
|
||||
@@ -1523,18 +1403,18 @@ const handleRemoveFromQueue = async () => {
|
||||
|
||||
// 是否可以上移
|
||||
const canMoveUp = computed(() => {
|
||||
if (!selectedQueueRow.value) return false
|
||||
if (!selectedQueueRow.value || selectedQueueRow.value.id == null) return false
|
||||
const index = originalQueueList.value.findIndex(
|
||||
item => item.queueOrder === selectedQueueRow.value.queueOrder
|
||||
item => item.id === selectedQueueRow.value.id
|
||||
)
|
||||
return index > 0
|
||||
})
|
||||
|
||||
// 是否可以下移
|
||||
const canMoveDown = computed(() => {
|
||||
if (!selectedQueueRow.value) return false
|
||||
if (!selectedQueueRow.value || selectedQueueRow.value.id == null) return false
|
||||
const index = originalQueueList.value.findIndex(
|
||||
item => item.queueOrder === selectedQueueRow.value.queueOrder
|
||||
item => item.id === selectedQueueRow.value.id
|
||||
)
|
||||
return index >= 0 && index < originalQueueList.value.length - 1
|
||||
})
|
||||
@@ -1614,7 +1494,7 @@ const stopWaitingTimer = () => {
|
||||
const getRowClassName = ({ row }) => {
|
||||
const classes = []
|
||||
if (row.status === '叫号中') classes.push('calling-row')
|
||||
if (selectedQueueRow.value && row.queueOrder === selectedQueueRow.value.queueOrder) {
|
||||
if (selectedQueueRow.value && row.id != null && row.id === selectedQueueRow.value.id) {
|
||||
classes.push('selected-row')
|
||||
}
|
||||
return classes.join(' ')
|
||||
@@ -2110,7 +1990,7 @@ const handleTestRule = () => {
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
// 初始化:优先从后端加载,失败则回退本地假数据
|
||||
// 初始化:从后端加载数据
|
||||
loadDataFromApi()
|
||||
startWaitingTimer()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user