Merge remote-tracking branch 'origin/develop' into zhaoyun

This commit is contained in:
2026-06-17 13:59:39 +08:00
30 changed files with 1665 additions and 39 deletions

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import java.util.List;
import java.util.Map;
public interface IEmrCompletenessAppService {
Map<String, Object> checkCompleteness(Long emrId, Long encounterId);
List<EmrCompletenessCheck> getCheckResults(Long emrId);
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrRevision;
import java.util.List;
public interface IEmrRevisionAppService {
EmrRevision recordRevision(EmrRevision revision);
List<EmrRevision> getRevisions(Long emrId);
EmrRevision getRevisionDetail(Long id);
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import java.util.List;
import java.util.Map;
public interface IEmrTimelinessAppService {
EmrTimelinessStatisticsDto checkTimeliness(Long encounterId);
Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize);
}

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrVersion;
import java.util.List;
import java.util.Map;
public interface IEmrVersionAppService {
EmrVersion saveVersion(EmrVersion version);
List<EmrVersion> getVersions(Long emrId);
Map<String, Object> compareVersions(Long versionId1, Long versionId2);
}

View File

@@ -0,0 +1,112 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.healthlink.his.document.domain.Emr;
import com.healthlink.his.document.service.IEmrService;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.service.IEmrCompletenessCheckService;
import com.healthlink.his.web.emr.appservice.IEmrCompletenessAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
public class EmrCompletenessAppServiceImpl implements IEmrCompletenessAppService {
@Resource
private IEmrCompletenessCheckService emrCompletenessCheckService;
@Resource
private IEmrService emrService;
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> checkCompleteness(Long emrId, Long encounterId) {
Emr emr = emrService.getById(emrId);
if (emr == null) {
throw new IllegalArgumentException("病历不存在: " + emrId);
}
List<EmrCompletenessCheck> checks = new ArrayList<>();
int total = 0;
int requiredPassed = 0;
int requiredTotal = 0;
String[][] checkDefs = {
{"chief_complaint", "basic", "true", "主诉"},
{"medical_history", "basic", "true", "现病史"},
{"past_history", "basic", "false", "既往史"},
{"physical_exam", "basic", "true", "体格检查"},
{"auxiliary_exam", "examination", "false", "辅助检查"},
{"diagnosis", "diagnosis", "true", "诊断"},
{"treatment_plan", "treatment", "true", "治疗计划"},
{"signature", "signature", "false", "签名"}
};
Map<String, Object> contentMap = parseContent(emr.getContextJson());
for (String[] def : checkDefs) {
total++;
boolean isRequired = Boolean.parseBoolean(def[2]);
if (isRequired) requiredTotal++;
boolean hasValue = contentMap.containsKey(def[0])
&& contentMap.get(def[0]) != null
&& !contentMap.get(def[0]).toString().trim().isEmpty();
String result = hasValue ? "PASS" : "FAIL";
String detail = def[3] + (hasValue ? " - 已填写" : " - 未填写");
if (!isRequired && !hasValue) {
detail = def[3] + " - 未填写(选填项)";
}
if (isRequired && hasValue) requiredPassed++;
EmrCompletenessCheck check = new EmrCompletenessCheck()
.setEmrId(emrId)
.setEncounterId(encounterId)
.setCheckItem(def[0])
.setCheckCategory(def[1])
.setIsRequired(isRequired)
.setCheckResult(result)
.setCheckDetail(detail)
.setCheckTime(new Date());
emrCompletenessCheckService.save(check);
checks.add(check);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("emrId", emrId);
result.put("encounterId", encounterId);
result.put("totalItems", total);
result.put("requiredTotal", requiredTotal);
result.put("requiredPassed", requiredPassed);
result.put("requiredFailed", requiredTotal - requiredPassed);
result.put("isComplete", requiredPassed == requiredTotal);
result.put("checks", checks);
return result;
}
@Override
public List<EmrCompletenessCheck> getCheckResults(Long emrId) {
return emrCompletenessCheckService.selectByEmrId(emrId);
}
private Map<String, Object> parseContent(String contextJson) {
Map<String, Object> map = new HashMap<>();
if (contextJson == null || contextJson.isEmpty()) {
return map;
}
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, Object> parsed = mapper.readValue(contextJson, Map.class);
map.putAll(parsed);
} catch (Exception e) {
map.put("raw", contextJson);
}
return map;
}
}

View File

@@ -0,0 +1,43 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class EmrRevisionAppServiceImpl implements IEmrRevisionAppService {
@Resource
private IEmrRevisionService emrRevisionService;
@Override
@Transactional(rollbackFor = Exception.class)
public EmrRevision recordRevision(EmrRevision revision) {
EmrRevision latest = emrRevisionService.selectLatest(revision.getEmrId());
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
revision.setRevisionNumber(nextNumber);
if (revision.getEncounterId() == null && latest != null) {
revision.setEncounterId(latest.getEncounterId());
}
revision.setCreateTime(new Date());
emrRevisionService.save(revision);
return revision;
}
@Override
public List<EmrRevision> getRevisions(Long emrId) {
return emrRevisionService.selectByEmrId(emrId);
}
@Override
public EmrRevision getRevisionDetail(Long id) {
return emrRevisionService.getById(id);
}
}

View File

@@ -0,0 +1,84 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import com.healthlink.his.emr.service.IEmrTimelinessService;
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class EmrTimelinessAppServiceImpl implements IEmrTimelinessAppService {
@Resource
private IEmrTimelinessService emrTimelinessService;
@Override
@Transactional(rollbackFor = Exception.class)
public EmrTimelinessStatisticsDto checkTimeliness(Long encounterId) {
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
if (encounterId != null) {
wrapper.eq(EmrTimeliness::getEncounterId, encounterId);
}
wrapper.eq(EmrTimeliness::getStatus, "PENDING");
List<EmrTimeliness> pendingList = emrTimelinessService.list(wrapper);
Date now = new Date();
int overdueCount = 0;
for (EmrTimeliness record : pendingList) {
if (record.getDeadlineTime() != null && now.after(record.getDeadlineTime())) {
record.setStatus("OVERDUE");
emrTimelinessService.updateById(record);
overdueCount++;
}
}
EmrTimelinessStatisticsDto stats = new EmrTimelinessStatisticsDto();
LambdaQueryWrapper<EmrTimeliness> countWrapper = new LambdaQueryWrapper<>();
if (encounterId != null) {
countWrapper.eq(EmrTimeliness::getEncounterId, encounterId);
}
long total = emrTimelinessService.count(countWrapper);
countWrapper.eq(EmrTimeliness::getStatus, "COMPLETED");
long completed = emrTimelinessService.count(countWrapper);
countWrapper.eq(EmrTimeliness::getStatus, "OVERDUE");
long overdue = emrTimelinessService.count(countWrapper);
long pending = total - completed - overdue;
double rate = total > 0 ? Math.round(completed * 10000.0 / total) / 100.0 : 0;
stats.setTotalCount(total);
stats.setCompletedCount(completed);
stats.setOverdueCount(overdue);
stats.setPendingCount(pending);
stats.setCompletionRate(rate);
return stats;
}
@Override
public Map<String, Object> getTimelinessAlerts(String emrType, String status, String departmentName, int pageNum, int pageSize) {
LambdaQueryWrapper<EmrTimeliness> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(status), EmrTimeliness::getStatus, status);
wrapper.eq(StringUtils.hasText(emrType), EmrTimeliness::getEmrType, emrType);
wrapper.eq(StringUtils.hasText(departmentName), EmrTimeliness::getDepartmentName, departmentName);
wrapper.orderByAsc(EmrTimeliness::getDeadlineTime);
Page<EmrTimeliness> page = emrTimelinessService.page(new Page<>(pageNum, pageSize), wrapper);
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", page.getTotal());
result.put("rows", page.getRecords());
return result;
}
}

View File

@@ -0,0 +1,92 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.healthlink.his.emr.domain.EmrVersion;
import com.healthlink.his.emr.service.IEmrVersionService;
import com.healthlink.his.web.emr.appservice.IEmrVersionAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class EmrVersionAppServiceImpl implements IEmrVersionAppService {
@Resource
private IEmrVersionService emrVersionService;
@Override
@Transactional(rollbackFor = Exception.class)
public EmrVersion saveVersion(EmrVersion version) {
EmrVersion latest = emrVersionService.selectLatest(version.getEmrId());
int nextNumber = (latest != null) ? latest.getVersionNumber() + 1 : 1;
version.setVersionNumber(nextNumber);
if (latest != null) {
version.setContentDiff(computeDiff(latest.getContentSnapshot(), version.getContentSnapshot()));
if (version.getEncounterId() == null) {
version.setEncounterId(latest.getEncounterId());
}
}
version.setCreateTime(new Date());
emrVersionService.save(version);
return version;
}
@Override
public List<EmrVersion> getVersions(Long emrId) {
return emrVersionService.selectByEmrId(emrId);
}
@Override
public Map<String, Object> compareVersions(Long versionId1, Long versionId2) {
EmrVersion v1 = emrVersionService.getById(versionId1);
EmrVersion v2 = emrVersionService.getById(versionId2);
if (v1 == null || v2 == null) {
throw new IllegalArgumentException("版本记录不存在");
}
String content1 = v1.getContentSnapshot() != null ? v1.getContentSnapshot() : "";
String content2 = v2.getContentSnapshot() != null ? v2.getContentSnapshot() : "";
String diff = computeDiff(content1, content2);
Map<String, Object> result = new LinkedHashMap<>();
result.put("version1", v1);
result.put("version2", v2);
result.put("diff", diff);
return result;
}
private String computeDiff(String oldContent, String newContent) {
if (oldContent == null) oldContent = "";
if (newContent == null) newContent = "";
if (oldContent.equals(newContent)) {
return "";
}
StringBuilder diff = new StringBuilder();
String[] oldLines = oldContent.split("\n");
String[] newLines = newContent.split("\n");
int maxLen = Math.max(oldLines.length, newLines.length);
for (int i = 0; i < maxLen; i++) {
String oldLine = i < oldLines.length ? oldLines[i] : "";
String newLine = i < newLines.length ? newLines[i] : "";
if (!oldLine.equals(newLine)) {
if (!oldLine.isEmpty()) {
diff.append("- ").append(oldLine).append("\n");
}
if (!newLine.isEmpty()) {
diff.append("+ ").append(newLine).append("\n");
}
}
}
return diff.toString();
}
}

View File

@@ -0,0 +1,38 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.web.emr.appservice.IEmrCompletenessAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/emr/completeness")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历完整性检查")
public class EmrCompletenessController {
private final IEmrCompletenessAppService emrCompletenessAppService;
@PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "执行病历完整性检查")
public R<Map<String, Object>> checkCompleteness(
@RequestParam("emrId") Long emrId,
@RequestParam("encounterId") Long encounterId) {
return R.ok(emrCompletenessAppService.checkCompleteness(emrId, encounterId));
}
@GetMapping("/results/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取完整性检查结果")
public R<?> getCheckResults(@PathVariable Long emrId) {
return R.ok(emrCompletenessAppService.getCheckResults(emrId));
}
}

View File

@@ -5,26 +5,47 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.web.emr.appservice.IEmrRevisionAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Map;
/**
* 病历修改留痕 Controller
*/
@RestController
@RequestMapping("/emr-revision")
@RequestMapping("/emr/revision")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历修改留痕")
public class EmrRevisionController {
private final IEmrRevisionService revisionService;
private final IEmrRevisionAppService emrRevisionAppService;
@PostMapping("/record")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "记录修改留痕")
public R<EmrRevision> recordRevision(@RequestBody EmrRevision revision) {
return R.ok(emrRevisionAppService.recordRevision(revision));
}
@GetMapping("/list/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取修改历史列表")
public R<?> getRevisions(@PathVariable Long emrId) {
return R.ok(emrRevisionAppService.getRevisions(emrId));
}
@GetMapping("/page")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "分页查询修改留痕")
public R<?> getPage(
@RequestParam(value = "emrId", required = false) Long emrId,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@@ -39,35 +60,16 @@ public class EmrRevisionController {
return R.ok(revisionService.page(new Page<>(pageNo, pageSize), w));
}
@GetMapping("/list")
public R<?> getList(@RequestParam("emrId") Long emrId) {
LambdaQueryWrapper<EmrRevision> w = new LambdaQueryWrapper<>();
w.eq(EmrRevision::getEmrId, emrId)
.orderByAsc(EmrRevision::getRevisionNumber);
return R.ok(revisionService.list(w));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取修订详情")
public R<?> getById(@PathVariable Long id) {
return R.ok(revisionService.getById(id));
}
@PostMapping("/record")
@Transactional(rollbackFor = Exception.class)
public R<?> recordRevision(@RequestBody EmrRevision revision) {
// 自动计算版本号
LambdaQueryWrapper<EmrRevision> w = new LambdaQueryWrapper<>();
w.eq(EmrRevision::getEmrId, revision.getEmrId())
.orderByDesc(EmrRevision::getRevisionNumber)
.last("LIMIT 1");
EmrRevision last = revisionService.getOne(w);
revision.setRevisionNumber(last == null ? 1 : last.getRevisionNumber() + 1);
revision.setCreateTime(new Date());
revisionService.save(revision);
return R.ok(revision);
return R.ok(emrRevisionAppService.getRevisionDetail(id));
}
@GetMapping("/compare")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "对比两个修订版本")
public R<?> compareRevisions(
@RequestParam("revisionId1") Long id1,
@RequestParam("revisionId2") Long id2) {

View File

@@ -0,0 +1,44 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import com.healthlink.his.web.emr.appservice.IEmrTimelinessAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/emr/timeliness")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历时限监控")
public class EmrTimelinessController {
private final IEmrTimelinessAppService emrTimelinessAppService;
@PostMapping("/check")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "执行病历时限检查")
public R<EmrTimelinessStatisticsDto> checkTimeliness(
@RequestParam(value = "encounterId", required = false) Long encounterId) {
return R.ok(emrTimelinessAppService.checkTimeliness(encounterId));
}
@GetMapping("/alerts")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取病历时限提醒列表")
public R<Map<String, Object>> getTimelinessAlerts(
@RequestParam(value = "emrType", required = false) String emrType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "departmentName", required = false) String departmentName,
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
return R.ok(emrTimelinessAppService.getTimelinessAlerts(emrType, status, departmentName, pageNum, pageSize));
}
}

View File

@@ -0,0 +1,44 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrVersion;
import com.healthlink.his.web.emr.appservice.IEmrVersionAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/emr/version")
@Slf4j
@AllArgsConstructor
@Tag(name = "病历版本管理")
public class EmrVersionController {
private final IEmrVersionAppService emrVersionAppService;
@PostMapping("/save")
@PreAuthorize("@ss.hasPermi('inpatient:emr:edit')")
@Operation(summary = "保存病历版本")
public R<EmrVersion> saveVersion(@RequestBody EmrVersion version) {
return R.ok(emrVersionAppService.saveVersion(version));
}
@GetMapping("/list/{emrId}")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "获取病历版本列表")
public R<?> getVersions(@PathVariable Long emrId) {
return R.ok(emrVersionAppService.getVersions(emrId));
}
@GetMapping("/compare")
@PreAuthorize("@ss.hasPermi('inpatient:emr:list')")
@Operation(summary = "对比两个版本")
public R<?> compareVersions(
@RequestParam("versionId1") Long versionId1,
@RequestParam("versionId2") Long versionId2) {
return R.ok(emrVersionAppService.compareVersions(versionId1, versionId2));
}
}

View File

@@ -467,8 +467,8 @@ public class InHospitalRegisterAppServiceImpl implements IInHospitalRegisterAppS
iAccountService.save(newCashAccount);
}
// 更新或创建非自费账户
String typeCode = StringUtils.isNotEmpty(inHospitalInfoDto.getTypeCoce())
? inHospitalInfoDto.getTypeCoce()
String typeCode = StringUtils.isNotEmpty(inHospitalInfoDto.getTypeCode())
? inHospitalInfoDto.getTypeCode()
: AccountType.PERSONAL_CASH_ACCOUNT.getCode();
if (contractAccount != null) {
contractAccount.setContractNo(inHospitalInfoDto.getContractNo());
@@ -617,11 +617,12 @@ public class InHospitalRegisterAppServiceImpl implements IInHospitalRegisterAppS
accountPersonalCash.setBalanceAmount(inHospitalInfoDto.getBalanceAmount()); // 账户余额
// 自费
if (selfFundedFlag) {
accountPersonalCash.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); // 自费合同编码
accountPersonalCash.setEncounterFlag(Whether.YES.getValue());
} else {
// 生成非自费的账号
Account accountNoSelfFunded = new Account();
accountNoSelfFunded.setTypeCode(inHospitalInfoDto.getTypeCoce()); // 账户类型
accountNoSelfFunded.setTypeCode(inHospitalInfoDto.getTypeCode()); // 账户类型
accountNoSelfFunded.setPatientId(
inHospitalInfoDto.getPatientId() != null ? inHospitalInfoDto.getPatientId() : patient.getId()); // 患者id
accountNoSelfFunded.setEncounterId(encounterReg.getId()); // 住院就诊id

View File

@@ -133,7 +133,7 @@ public class InHospitalInfoDto {
/**
* 账户类型编码
*/
private String typeCoce;
private String typeCode;
/** 账户余额 */
private BigDecimal balanceAmount;

View File

@@ -0,0 +1,26 @@
-- V64: 病历版本管理
CREATE TABLE IF NOT EXISTS emr_version (
id BIGSERIAL PRIMARY KEY,
emr_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
version_number INT NOT NULL DEFAULT 1,
content_snapshot TEXT,
content_diff TEXT,
emr_type VARCHAR(50),
emr_title VARCHAR(200),
operator_id BIGINT,
operator_name VARCHAR(64),
save_reason VARCHAR(200),
tenant_id BIGINT DEFAULT 0,
delete_flag VARCHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE emr_version IS '病历版本管理(不可删除,三甲评审要求)';
COMMENT ON COLUMN emr_version.version_number IS '版本号,递增';
COMMENT ON COLUMN emr_version.content_snapshot IS '完整内容快照';
COMMENT ON COLUMN emr_version.content_diff IS '与上一版本的差异';
COMMENT ON COLUMN emr_version.save_reason IS '保存原因';
CREATE INDEX IF NOT EXISTS idx_ev_emr ON emr_version(emr_id);
CREATE INDEX IF NOT EXISTS idx_ev_encounter ON emr_version(encounter_id);
CREATE INDEX IF NOT EXISTS idx_ev_version ON emr_version(emr_id, version_number);

View File

@@ -38,7 +38,7 @@
AND aper.delete_flag = '0'
LEFT JOIN adm_account AS aa ON aa.encounter_id = ae.ID
AND aa.delete_flag = '0'
AND aa.type_code = '04'
AND aa.encounter_flag = 1
LEFT JOIN adm_encounter AS ambae ON ae.amb_encounter_id = ambae.ID
LEFT JOIN adm_organization AS ao ON ao.ID = ambae.organization_id
AND ao.delete_flag = '0'
@@ -142,7 +142,7 @@
LEFT JOIN adm_account AS aa
ON aa.encounter_id = ae.ID
AND aa.delete_flag = '0'
AND aa.type_code = #{accountType}
AND aa.encounter_flag = 1
WHERE ae.ID = #{encounterId}
</select>

View File

@@ -0,0 +1,37 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("emr_version")
@Accessors(chain = true)
public class EmrVersion extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
private Long emrId;
private Long encounterId;
private Integer versionNumber;
private String contentSnapshot;
private String contentDiff;
private String emrType;
private String emrTitle;
private Long operatorId;
private String operatorName;
private String saveReason;
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.emr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.emr.domain.EmrVersion;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface EmrVersionMapper extends BaseMapper<EmrVersion> {
List<EmrVersion> selectByEmrId(@Param("emrId") Long emrId);
EmrVersion selectLatest(@Param("emrId") Long emrId);
}

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.emr.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.emr.domain.EmrVersion;
import java.util.List;
public interface IEmrVersionService extends IService<EmrVersion> {
List<EmrVersion> selectByEmrId(Long emrId);
EmrVersion selectLatest(Long emrId);
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrVersion;
import com.healthlink.his.emr.mapper.EmrVersionMapper;
import com.healthlink.his.emr.service.IEmrVersionService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmrVersionServiceImpl
extends ServiceImpl<EmrVersionMapper, EmrVersion>
implements IEmrVersionService {
@Override
public List<EmrVersion> selectByEmrId(Long emrId) {
return baseMapper.selectByEmrId(emrId);
}
@Override
public EmrVersion selectLatest(Long emrId) {
return baseMapper.selectLatest(emrId);
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.emr.mapper.EmrVersionMapper">
<select id="selectByEmrId" resultType="com.healthlink.his.emr.domain.EmrVersion">
SELECT * FROM emr_version
WHERE emr_id = #{emrId} AND delete_flag = '0'
ORDER BY version_number DESC
</select>
<select id="selectLatest" resultType="com.healthlink.his.emr.domain.EmrVersion">
SELECT * FROM emr_version
WHERE emr_id = #{emrId} AND delete_flag = '0'
ORDER BY version_number DESC
LIMIT 1
</select>
</mapper>

View File

@@ -93,3 +93,11 @@ export function saveAnesSummary(data) {
export function getAnesSummary(recordId) {
return request({ url: '/api/v1/anesthesia/summary/' + recordId, method: 'get' })
}
export function recordPostopFollowup(data) {
return request({ url: '/api/v1/anesthesia/postop-followup', method: 'post', data })
}
export function getPostopFollowups(encounterId) {
return request({ url: '/api/v1/anesthesia/postop-followup/' + encounterId, method: 'get' })
}

View File

@@ -2,6 +2,20 @@ import request from "@/utils/request"
export function getTimelinessByEncounter(encounterId) { return request({ url: "/emr-revision/timeliness/" + encounterId, method: "get" }) }
export function getTimelinessStatistics(params) { return request({ url: "/emr-revision/statistics", method: "get", params }) }
export function getPendingEmrCount(params) { return request({ url: "/emr-archive/pending-count", method: "get", params }) }
// 查询超期病历列表
export function getOverdueList(params) { return request({ url: "/emr-archive/overdue/list", method: "get", params }) }
export function recordEmrRevision(data) { return request({ url: "/emr/revision/record", method: "post", data }) }
export function getEmrRevisionList(emrId) { return request({ url: "/emr/revision/list/" + emrId, method: "get" }) }
export function getEmrRevisionPage(params) { return request({ url: "/emr/revision/page", method: "get", params }) }
export function getEmrRevisionDetail(id) { return request({ url: "/emr/revision/" + id, method: "get" }) }
export function compareEmrRevisions(id1, id2) { return request({ url: "/emr/revision/compare", method: "get", params: { revisionId1: id1, revisionId2: id2 } }) }
export function saveEmrVersion(data) { return request({ url: "/emr/version/save", method: "post", data }) }
export function getEmrVersionList(emrId) { return request({ url: "/emr/version/list/" + emrId, method: "get" }) }
export function compareEmrVersions(id1, id2) { return request({ url: "/emr/version/compare", method: "get", params: { versionId1: id1, versionId2: id2 } }) }
export function checkCompleteness(emrId, encounterId) { return request({ url: "/emr/completeness/check", method: "post", params: { emrId, encounterId } }) }
export function getCompletenessResults(emrId) { return request({ url: "/emr/completeness/results/" + emrId, method: "get" }) }
export function checkTimeliness(encounterId) { return request({ url: "/emr/timeliness/check", method: "post", params: { encounterId } }) }
export function getTimelinessAlerts(params) { return request({ url: "/emr/timeliness/alerts", method: "get", params }) }

View File

@@ -1,4 +1,5 @@
import request from '@/utils/request'
export function createRevision(data) { return request({ url: '/api/v1/emr/revision', method: 'post', data }) }
export function getRevisionHistory(emrId) { return request({ url: '/api/v1/emr/revision/' + emrId, method: 'get' }) }
export function executeCompletenessCheck(emrId) { return request({ url: '/api/v1/emr/completeness-check/' + emrId, method: 'post' }) }
@@ -7,3 +8,9 @@ export function getTimelinessByEncounter(encounterId) { return request({ url: '/
export function getOverdueList() { return request({ url: '/api/v1/emr/timeliness/overdue', method: 'get' }) }
export function getTimelinessStatistics(params) { return request({ url: '/api/v1/emr/timeliness/statistics', method: 'get', params }) }
export function checkTimeliness(data) { return request({ url: '/api/v1/emr/timeliness/check', method: 'post', data }) }
export function recordEmrRevision(data) { return request({ url: '/emr/revision/record', method: 'post', data }) }
export function getEmrRevisionList(emrId) { return request({ url: '/emr/revision/list/' + emrId, method: 'get' }) }
export function getEmrRevisionPage(params) { return request({ url: '/emr/revision/page', method: 'get', params }) }
export function getEmrRevisionDetail(id) { return request({ url: '/emr/revision/' + id, method: 'get' }) }
export function compareEmrRevisions(id1, id2) { return request({ url: '/emr/revision/compare', method: 'get', params: { revisionId1: id1, revisionId2: id2 } }) }

View File

@@ -0,0 +1,156 @@
<template>
<div class="app-container">
<el-card shadow="hover" class="mb8">
<template #header>
<span>执行完整性检查</span>
</template>
<el-form :inline="true" :model="checkForm">
<el-form-item label="病历ID">
<el-input v-model="checkForm.emrId" placeholder="请输入病历ID" clearable />
</el-form-item>
<el-form-item label="就诊ID">
<el-input v-model="checkForm.encounterId" placeholder="请输入就诊ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="checkLoading" @click="handleCheck">
执行检查
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row v-if="checkResult" :gutter="20" class="mb8">
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="检查项总数" :value="checkResult.totalItems" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="必填项总数" :value="checkResult.requiredTotal" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="必填项通过" :value="checkResult.requiredPassed" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="是否完整" :value="checkResult.isComplete ? '是' : '否'" />
</el-card>
</el-col>
</el-row>
<el-card v-if="checkResult && checkResult.requiredFailed > 0" shadow="hover" class="mb8">
<template #header>
<span style="color: #e6a23c;">不合格项提醒</span>
</template>
<el-alert
v-for="item in failedItems"
:key="item.id"
:title="item.checkItem"
:description="item.checkDetail"
type="warning"
show-icon
class="mb4"
/>
</el-card>
<el-card shadow="hover">
<template #header>
<span>检查结果明细</span>
</template>
<el-table v-loading="resultLoading" :data="resultList">
<el-table-column label="检查项" prop="checkItem" width="150" />
<el-table-column label="分类" prop="checkCategory" width="120">
<template #default="scope">
<el-tag>{{ categoryMap[scope.row.checkCategory] || scope.row.checkCategory }}</el-tag>
</template>
</el-table-column>
<el-table-column label="是否必填" prop="isRequired" width="100">
<template #default="scope">
<el-tag :type="scope.row.isRequired ? 'danger' : 'info'">
{{ scope.row.isRequired ? '必填' : '选填' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="检查结果" prop="checkResult" width="120">
<template #default="scope">
<el-tag :type="scope.row.checkResult === 'PASS' ? 'success' : 'danger'">
{{ scope.row.checkResult === 'PASS' ? '通过' : '不合格' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="详情" prop="checkDetail" min-width="200" show-overflow-tooltip />
<el-table-column label="检查时间" prop="checkTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { checkCompleteness, getCompletenessResults } from '@/api/emr'
import { ElMessage } from 'element-plus'
const checkLoading = ref(false)
const resultLoading = ref(false)
const checkResult = ref(null)
const resultList = ref([])
const checkForm = reactive({ emrId: '', encounterId: '' })
const categoryMap = {
basic: '基本信息',
examination: '检查',
diagnosis: '诊断',
treatment: '治疗',
signature: '签名'
}
const failedItems = computed(() => {
if (!checkResult.value || !checkResult.value.checks) return []
return checkResult.value.checks.filter(c => c.checkResult === 'FAIL')
})
const handleCheck = async () => {
if (!checkForm.emrId) {
ElMessage.warning('请输入病历ID')
return
}
if (!checkForm.encounterId) {
ElMessage.warning('请输入就诊ID')
return
}
checkLoading.value = true
try {
const res = await checkCompleteness(checkForm.emrId, checkForm.encounterId)
checkResult.value = res.data || res
resultList.value = checkResult.value.checks || []
ElMessage.success(checkResult.value.isComplete ? '病历完整性检查通过' : '病历完整性检查未通过,存在不合格项')
} catch (e) {
ElMessage.error('执行检查失败')
} finally {
checkLoading.value = false
}
}
const loadResults = async () => {
if (!checkForm.emrId) return
resultLoading.value = true
try {
const res = await getCompletenessResults(checkForm.emrId)
resultList.value = res.data || res || []
} catch (e) {
ElMessage.error('获取检查结果失败')
} finally {
resultLoading.value = false
}
}
</script>
<style scoped>
.mb8 { margin-bottom: 8px; }
.mb4 { margin-bottom: 4px; }
</style>

View File

@@ -32,8 +32,7 @@
<div class="table-container">
<vxe-table
:data="treatHospitalizedData"
style="width: 100%"
height="100%"
min-width="900px"
show-overflow="title"
>
<vxe-column
@@ -44,15 +43,18 @@
/>
<vxe-column
field="patientName"
min-width="100"
align="center"
title="患者姓名"
/>
<vxe-column
field="genderEnum_enumText"
width="70"
title="性别"
align="center"
/>
<vxe-column
width="80"
title="年龄"
align="center"
>
@@ -71,16 +73,19 @@
</vxe-column>
<vxe-column
field="requestTime"
min-width="160"
align="center"
title="申请时间"
/>
<vxe-column
field="sourceName"
min-width="120"
align="center"
title="申请来源"
/>
<vxe-column
field="wardName"
min-width="120"
align="center"
title="入院病区"
/>
@@ -262,6 +267,7 @@ getList();
}
.table-container {
padding: 8px 16px;
overflow-x: auto;
}
}
</style>

View File

@@ -149,7 +149,7 @@ const router = useRouter();
const emits = defineEmits(['okAct', 'cancelAct']);
const props = defineProps({
title: '',
title: { type: String, default: '' },
registrationType: {
type: [String, Boolean, Number], // 根据实际类型调整
default: null, // 或者 false、'' 等
@@ -466,7 +466,7 @@ const handleEditSubmit = () => {
inWayCode: formData.inWayCode,
startTime: formData.startTime,
contractNo: formData.contractNo,
typeCoce: formData.typeCoce,
typeCode: formData.typeCode,
};
updateRegistration(params).then((res) => {
if (res.code == 200) {

View File

@@ -0,0 +1,277 @@
<template>
<div class="emr-revision-track">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<span>修改留痕记录</span>
<div>
<el-button v-if="compareIds.length === 2" type="primary" size="small" @click="handleCompare">对比选中</el-button>
<el-button type="info" size="small" @click="handleRefresh">刷新</el-button>
</div>
</div>
</template>
<el-timeline v-if="revisions.length > 0">
<el-timeline-item
v-for="item in revisions"
:key="item.id"
:timestamp="item.createTime"
placement="top"
:type="getTimelineType(item.operationType)"
size="large"
>
<el-card shadow="never" class="revision-card">
<div class="revision-header">
<div class="revision-meta">
<el-tag size="small" :type="getOpTagType(item.operationType)">{{ getOpLabel(item.operationType) }}</el-tag>
<span class="revision-number">版本 #{{ item.revisionNumber }}</span>
<span class="revision-operator">{{ item.operatorName }}</span>
</div>
<div class="revision-actions">
<el-checkbox
:model-value="compareIds.includes(item.id)"
:disabled="!compareIds.includes(item.id) && compareIds.length >= 2"
@change="(val) => toggleCompare(item.id, val)"
/>
<el-button link type="primary" size="small" @click="handleViewDetail(item)">查看详情</el-button>
</div>
</div>
<div v-if="item.diffContent" class="revision-diff">
<div class="diff-label">修改内容</div>
<pre class="diff-content">{{ item.diffContent }}</pre>
</div>
<div v-if="item.snapshotContent" class="revision-snapshot">
<div class="diff-label">快照内容</div>
<pre class="diff-content">{{ truncateContent(item.snapshotContent) }}</pre>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无修改记录" />
</el-card>
<el-dialog v-model="detailVisible" title="修订详情" width="700px" destroy-on-close top="5vh">
<div v-if="currentRevision" class="detail-content">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="版本号">{{ currentRevision.revisionNumber }}</el-descriptions-item>
<el-descriptions-item label="操作类型">
<el-tag size="small" :type="getOpTagType(currentRevision.operationType)">{{ getOpLabel(currentRevision.operationType) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="操作人">{{ currentRevision.operatorName }}</el-descriptions-item>
<el-descriptions-item label="操作时间">{{ currentRevision.createTime }}</el-descriptions-item>
<el-descriptions-item label="病历ID">{{ currentRevision.emrId }}</el-descriptions-item>
<el-descriptions-item label="就诊ID">{{ currentRevision.encounterId }}</el-descriptions-item>
</el-descriptions>
<div v-if="currentRevision.diffContent" style="margin-top: 16px">
<div class="diff-label">修改内容</div>
<pre class="detail-pre">{{ currentRevision.diffContent }}</pre>
</div>
<div v-if="currentRevision.snapshotContent" style="margin-top: 16px">
<div class="diff-label">快照内容</div>
<pre class="detail-pre">{{ currentRevision.snapshotContent }}</pre>
</div>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="compareVisible" title="修订对比" width="900px" destroy-on-close top="5vh">
<div v-if="compareData" class="compare-content">
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span>版本 #{{ compareData.revision1?.revisionNumber }} {{ compareData.revision1?.operatorName }}</span>
</template>
<pre class="compare-pre">{{ compareData.revision1?.snapshotContent || '无快照' }}</pre>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span>版本 #{{ compareData.revision2?.revisionNumber }} {{ compareData.revision2?.operatorName }}</span>
</template>
<pre class="compare-pre">{{ compareData.revision2?.snapshotContent || '无快照' }}</pre>
</el-card>
</el-col>
</el-row>
</div>
<template #footer>
<el-button @click="compareVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getEmrRevisionList, getEmrRevisionDetail, compareEmrRevisions } from '@/api/emr'
const props = defineProps({
emrId: { type: [Number, String], default: null }
})
const loading = ref(false)
const revisions = ref([])
const compareIds = ref([])
const detailVisible = ref(false)
const compareVisible = ref(false)
const currentRevision = ref(null)
const compareData = ref(null)
const OP_LABEL_MAP = { CREATE: '创建', EDIT: '编辑', APPROVE: '审批', SIGN: '签名' }
const OP_TAG_MAP = { CREATE: 'success', EDIT: '', APPROVE: 'warning', SIGN: 'info' }
const TIMELINE_MAP = { CREATE: 'primary', EDIT: 'success', APPROVE: 'warning', SIGN: 'info' }
function getOpLabel(type) { return OP_LABEL_MAP[type] || type }
function getOpTagType(type) { return OP_TAG_MAP[type] || '' }
function getTimelineType(type) { return TIMELINE_MAP[type] || '' }
function truncateContent(content) {
if (!content) return ''
return content.length > 200 ? content.substring(0, 200) + '...' : content
}
function loadRevisions() {
if (!props.emrId) return
loading.value = true
getEmrRevisionList(props.emrId).then(res => {
revisions.value = res.data || []
}).catch(() => {
ElMessage.error('查询修改记录失败')
}).finally(() => {
loading.value = false
})
}
function handleRefresh() {
compareIds.value = []
loadRevisions()
}
function toggleCompare(id, checked) {
if (checked) {
compareIds.value.push(id)
} else {
compareIds.value = compareIds.value.filter(i => i !== id)
}
}
function handleViewDetail(item) {
loading.value = true
getEmrRevisionDetail(item.id).then(res => {
currentRevision.value = res.data
detailVisible.value = true
}).catch(() => {
ElMessage.error('查询详情失败')
}).finally(() => {
loading.value = false
})
}
function handleCompare() {
if (compareIds.value.length !== 2) {
ElMessage.warning('请选择两个版本进行对比')
return
}
loading.value = true
compareEmrRevisions(compareIds.value[0], compareIds.value[1]).then(res => {
compareData.value = res.data
compareVisible.value = true
}).catch(() => {
ElMessage.error('对比查询失败')
}).finally(() => {
loading.value = false
})
}
watch(() => props.emrId, () => { loadRevisions() })
onMounted(() => { loadRevisions() })
</script>
<style scoped>
.emr-revision-track {
padding: 12px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.revision-card {
margin-bottom: 0;
}
.revision-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.revision-meta {
display: flex;
align-items: center;
gap: 8px;
}
.revision-number {
font-weight: 600;
color: #303133;
}
.revision-operator {
color: #606266;
}
.revision-actions {
display: flex;
align-items: center;
gap: 8px;
}
.diff-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.diff-content {
background: #f5f7fa;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
max-height: 120px;
overflow-y: auto;
}
.detail-content {
max-height: 60vh;
overflow-y: auto;
}
.detail-pre {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.compare-content {
max-height: 65vh;
overflow-y: auto;
}
.compare-pre {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
max-height: 50vh;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="emr-timeliness-monitor">
<el-row :gutter="16" class="stats-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value total">{{ stats.totalCount || 0 }}</div>
<div class="stat-label">病历总数</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value completed">{{ stats.completedCount || 0 }}</div>
<div class="stat-label">已完成</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value pending">{{ stats.pendingCount || 0 }}</div>
<div class="stat-label">待完成</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value overdue">{{ stats.overdueCount || 0 }}</div>
<div class="stat-label">已超时</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" class="filter-card">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="病历类型">
<el-select v-model="queryParams.emrType" clearable placeholder="全部" style="width: 160px">
<el-option label="入院记录" value="ADMISSION" />
<el-option label="首次病程" value="FIRST_COURSE" />
<el-option label="日常病程" value="DAILY_COURSE" />
<el-option label="出院记录" value="DISCHARGE" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" clearable placeholder="全部" style="width: 140px">
<el-option label="待完成" value="PENDING" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已超时" value="OVERDUE" />
</el-select>
</el-form-item>
<el-form-item label="科室">
<el-input v-model="queryParams.departmentName" clearable placeholder="输入科室名" style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
搜索
</el-button>
<el-button @click="handleReset">
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="table-card">
<template #header>
<div class="card-header">
<span>时限提醒列表</span>
<el-button type="primary" size="small" @click="handleCheckAll">
执行检查
</el-button>
</div>
</template>
<el-table v-loading="loading" :data="alertList" stripe border size="default">
<el-table-column prop="emrType" label="病历类型" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getEmrTypeTag(row.emrType)">
{{ getEmrTypeLabel(row.emrType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="patientId" label="患者ID" width="100" align="center" />
<el-table-column prop="doctorName" label="主治医生" width="110" align="center" />
<el-table-column prop="departmentName" label="科室" width="130" align="center" />
<el-table-column prop="requiredHours" label="时限(小时)" width="100" align="center" />
<el-table-column prop="deadlineTime" label="截止时间" width="170" align="center" />
<el-table-column prop="actualCompleteTime" label="完成时间" width="170" align="center">
<template #default="{ row }">
{{ row.actualCompleteTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getStatusTag(row.status)">
{{ getStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="loadAlerts"
@current-change="loadAlerts"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { checkTimeliness, getTimelinessAlerts } from '@/api/emr'
const loading = ref(false)
const stats = ref({})
const alertList = ref([])
const total = ref(0)
const queryParams = reactive({
emrType: '',
status: '',
departmentName: '',
pageNum: 1,
pageSize: 20
})
const EMR_TYPE_MAP = {
ADMISSION: '入院记录',
FIRST_COURSE: '首次病程',
DAILY_COURSE: '日常病程',
DISCHARGE: '出院记录'
}
const EMR_TYPE_TAG = {
ADMISSION: 'primary',
FIRST_COURSE: 'success',
DAILY_COURSE: 'warning',
DISCHARGE: 'info'
}
const STATUS_MAP = {
PENDING: '待完成',
COMPLETED: '已完成',
OVERDUE: '已超时'
}
const STATUS_TAG = {
PENDING: 'warning',
COMPLETED: 'success',
OVERDUE: 'danger'
}
function getEmrTypeLabel(type) { return EMR_TYPE_MAP[type] || type }
function getEmrTypeTag(type) { return EMR_TYPE_TAG[type] || '' }
function getStatusLabel(s) { return STATUS_MAP[s] || s }
function getStatusTag(s) { return STATUS_TAG[s] || '' }
function loadStats() {
checkTimeliness(null).then(res => {
stats.value = res.data || {}
})
}
function loadAlerts() {
loading.value = true
const params = { ...queryParams }
if (!params.emrType) delete params.emrType
if (!params.status) delete params.status
if (!params.departmentName) delete params.departmentName
getTimelinessAlerts(params).then(res => {
alertList.value = res.data?.rows || []
total.value = res.data?.total || 0
}).catch(() => {
ElMessage.error('查询时限提醒失败')
}).finally(() => {
loading.value = false
})
}
function handleSearch() {
queryParams.pageNum = 1
loadAlerts()
}
function handleReset() {
queryParams.emrType = ''
queryParams.status = ''
queryParams.departmentName = ''
queryParams.pageNum = 1
loadAlerts()
}
function handleCheckAll() {
loading.value = true
checkTimeliness(null).then(res => {
stats.value = res.data || {}
ElMessage.success('时限检查完成')
loadAlerts()
}).catch(() => {
ElMessage.error('执行检查失败')
}).finally(() => {
loading.value = false
})
}
onMounted(() => {
loadStats()
loadAlerts()
})
</script>
<style scoped>
.emr-timeliness-monitor {
padding: 16px;
}
.stats-row {
margin-bottom: 16px;
}
.stat-card {
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1.4;
}
.stat-value.total { color: #409eff; }
.stat-value.completed { color: #67c23a; }
.stat-value.pending { color: #e6a23c; }
.stat-value.overdue { color: #f56c6c; }
.stat-label {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.filter-card {
margin-bottom: 16px;
}
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.table-card .el-pagination {
margin-top: 16px;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<div class="emr-version-compare">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<span>历史版本管理</span>
<div>
<el-button
v-if="compareIds.length === 2"
type="primary"
size="small"
@click="handleCompare"
>
对比选中版本
</el-button>
<el-button
type="info"
size="small"
@click="handleRefresh"
>
刷新
</el-button>
</div>
</div>
</template>
<el-table
v-if="versions.length > 0"
:data="versions"
border
size="small"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="40" />
<el-table-column label="版本号" width="90" align="center">
<template #default="{ row }">
<el-tag size="small">v{{ row.versionNumber }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="emrTitle" label="病历标题" show-overflow-tooltip />
<el-table-column prop="operatorName" label="操作人" width="100" />
<el-table-column prop="saveReason" label="保存原因" show-overflow-tooltip />
<el-table-column prop="createTime" label="保存时间" width="170" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleViewDetail(row)">
查看
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无历史版本" />
</el-card>
<el-dialog
v-model="detailVisible"
title="版本详情"
width="700px"
destroy-on-close
top="5vh"
>
<div v-if="currentVersion" class="detail-content">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="版本号">
v{{ currentVersion.versionNumber }}
</el-descriptions-item>
<el-descriptions-item label="操作人">
{{ currentVersion.operatorName }}
</el-descriptions-item>
<el-descriptions-item label="保存时间">
{{ currentVersion.createTime }}
</el-descriptions-item>
<el-descriptions-item label="保存原因">
{{ currentVersion.saveReason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="病历ID">
{{ currentVersion.emrId }}
</el-descriptions-item>
<el-descriptions-item label="就诊ID">
{{ currentVersion.encounterId }}
</el-descriptions-item>
</el-descriptions>
<div v-if="currentVersion.contentSnapshot" style="margin-top: 16px">
<div class="diff-label">内容快照</div>
<pre class="detail-pre">{{ currentVersion.contentSnapshot }}</pre>
</div>
<div v-if="currentVersion.contentDiff" style="margin-top: 16px">
<div class="diff-label">与上一版本差异</div>
<pre class="detail-pre diff-text">{{ currentVersion.contentDiff }}</pre>
</div>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-model="compareVisible"
title="版本对比"
width="900px"
destroy-on-close
top="5vh"
>
<div v-if="compareData" class="compare-content">
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span>版本 #{{ compareData.version1?.versionNumber }} {{ compareData.version1?.operatorName }}</span>
</template>
<pre class="compare-pre">{{ compareData.version1?.contentSnapshot || '无快照' }}</pre>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<span>版本 #{{ compareData.version2?.versionNumber }} {{ compareData.version2?.operatorName }}</span>
</template>
<pre class="compare-pre">{{ compareData.version2?.contentSnapshot || '无快照' }}</pre>
</el-card>
</el-col>
</el-row>
<div v-if="compareData.diff" style="margin-top: 16px">
<el-card shadow="never">
<template #header>
<span>差异详情</span>
</template>
<pre class="compare-pre diff-text">{{ compareData.diff }}</pre>
</el-card>
</div>
</div>
<template #footer>
<el-button @click="compareVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getEmrVersionList, compareEmrVersions } from '@/api/emr'
const props = defineProps({
emrId: { type: [Number, String], default: null }
})
const loading = ref(false)
const versions = ref([])
const compareIds = ref([])
const detailVisible = ref(false)
const compareVisible = ref(false)
const currentVersion = ref(null)
const compareData = ref(null)
function loadVersions() {
if (!props.emrId) return
loading.value = true
getEmrVersionList(props.emrId).then(res => {
versions.value = res.data || []
}).catch(() => {
ElMessage.error('查询版本记录失败')
}).finally(() => {
loading.value = false
})
}
function handleRefresh() {
compareIds.value = []
loadVersions()
}
function handleSelectionChange(selection) {
compareIds.value = selection.map(item => item.id)
}
function handleViewDetail(item) {
currentVersion.value = item
detailVisible.value = true
}
function handleCompare() {
if (compareIds.value.length !== 2) {
ElMessage.warning('请选择两个版本进行对比')
return
}
loading.value = true
compareEmrVersions(compareIds.value[0], compareIds.value[1]).then(res => {
compareData.value = res.data
compareVisible.value = true
}).catch(() => {
ElMessage.error('对比查询失败')
}).finally(() => {
loading.value = false
})
}
watch(() => props.emrId, () => { loadVersions() })
onMounted(() => { loadVersions() })
</script>
<style scoped>
.emr-version-compare {
padding: 12px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.diff-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.detail-content {
max-height: 60vh;
overflow-y: auto;
}
.detail-pre {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.diff-text {
color: #e6a23c;
}
.compare-content {
max-height: 65vh;
overflow-y: auto;
}
.compare-pre {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
max-height: 50vh;
overflow-y: auto;
}
</style>