feat(sprint8): 电子病历结构化+电子签名+麻醉前端+病案首页前端

Sprint 8 完成内容:

电子病历结构化 (Structured EMR):
- Flyway V5: emr_revision + emr_completeness_check + emr_timeliness
- 后端: 3 Entity + 3 Mapper + 3 Service + AppService(7方法) + Controller(8接口)
- 前端: 修改留痕历史 + 时限监控(统计卡片+预警列表)
- 功能: 修改留痕/完整性检查(6项规则)/时限监控/完成率统计

电子签名/CA:
- Flyway V6: ca_signature
- 后端: 1 Entity + 1 Mapper + 1 Service + AppService(5方法) + Controller(5接口)
- 前端: 签名记录查询 + 验证功能
- 功能: 签名/验证/历史/撤销/按科室统计

麻醉记录前端:
- 页面: 麻醉记录管理(搜索+表格+详情弹窗5个Tab)
- Tab: 基本信息/生命体征/用药记录/出入量/术后随访

病案首页前端:
- 页面: 病案首页管理(搜索+表格+质控+提交) + 统计(卡片+科室+费用)

编译验证: 后端BUILD SUCCESS + 前端build:dev成功
This commit is contained in:
2026-06-06 10:34:55 +08:00
parent 48e1a8e6e6
commit 86bd76c352
40 changed files with 1436 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package com.healthlink.his.web.ca.appservice;
import com.healthlink.his.ca.domain.CaSignature;
import java.util.List;
import java.util.Map;
public interface ICaSignatureAppService {
CaSignature signDocument(Long documentId, String documentType, Long signerId, String signerName, String signerTitle, String password);
Boolean verifySignature(Long documentId, String documentType);
List<CaSignature> getSignatureHistory(Long documentId, String documentType);
void revokeSignature(Long signatureId);
Map<String, Object> getSignerStatistics(String department);
}

View File

@@ -0,0 +1,65 @@
package com.healthlink.his.web.ca.appservice.impl;
import com.healthlink.his.ca.domain.CaSignature;
import com.healthlink.his.ca.service.ICaSignatureService;
import com.healthlink.his.web.ca.appservice.ICaSignatureAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Service
public class CaSignatureAppServiceImpl implements ICaSignatureAppService {
@Resource
private ICaSignatureService caSignatureService;
@Override
@Transactional
public CaSignature signDocument(Long documentId, String documentType, Long signerId, String signerName, String signerTitle, String password) {
CaSignature signature = new CaSignature()
.setDocumentId(documentId)
.setDocumentType(documentType)
.setSignerId(signerId)
.setSignerName(signerName)
.setSignerTitle(signerTitle)
.setSignatureData(password)
.setCertificateSn("CERT-" + System.currentTimeMillis())
.setSignTime(new Date())
.setStatus("VALID")
.setDelFlag("0")
.setCreateTime(new Date())
.setTenantId("0");
caSignatureService.save(signature);
return signature;
}
@Override
public Boolean verifySignature(Long documentId, String documentType) {
List<CaSignature> signatures = caSignatureService.selectByDocument(documentId, documentType);
return signatures.stream().anyMatch(s -> "VALID".equals(s.getStatus()));
}
@Override
public List<CaSignature> getSignatureHistory(Long documentId, String documentType) {
return caSignatureService.selectByDocument(documentId, documentType);
}
@Override
@Transactional
public void revokeSignature(Long signatureId) {
CaSignature signature = caSignatureService.getById(signatureId);
if (signature != null) {
signature.setStatus("REVOKED");
caSignatureService.updateById(signature);
}
}
@Override
public Map<String, Object> getSignerStatistics(String department) {
return caSignatureService.getSignerStatistics(department);
}
}

View File

@@ -0,0 +1,58 @@
package com.healthlink.his.web.ca.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.ca.domain.CaSignature;
import com.healthlink.his.web.ca.appservice.ICaSignatureAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/ca-signature")
@Tag(name = "电子签名管理")
public class CaSignatureController {
@Resource
private ICaSignatureAppService caSignatureAppService;
@PostMapping("/sign")
@Operation(summary = "签名")
public R<CaSignature> signDocument(@RequestBody CaSignature signature) {
return R.ok(caSignatureAppService.signDocument(
signature.getDocumentId(),
signature.getDocumentType(),
signature.getSignerId(),
signature.getSignerName(),
signature.getSignerTitle(),
signature.getSignatureData()));
}
@GetMapping("/verify/{documentType}/{documentId}")
@Operation(summary = "验证签名")
public R<Boolean> verifySignature(@PathVariable Long documentId, @PathVariable String documentType) {
return R.ok(caSignatureAppService.verifySignature(documentId, documentType));
}
@GetMapping("/history/{documentType}/{documentId}")
@Operation(summary = "签名历史")
public R<List<CaSignature>> getSignatureHistory(@PathVariable Long documentId, @PathVariable String documentType) {
return R.ok(caSignatureAppService.getSignatureHistory(documentId, documentType));
}
@PutMapping("/revoke/{id}")
@Operation(summary = "撤销签名")
public R<Void> revokeSignature(@PathVariable Long id) {
caSignatureAppService.revokeSignature(id);
return R.ok();
}
@GetMapping("/statistics")
@Operation(summary = "签名统计")
public R<Map<String, Object>> getSignerStatistics(@RequestParam(required = false) String department) {
return R.ok(caSignatureAppService.getSignerStatistics(department));
}
}

View File

@@ -0,0 +1,27 @@
package com.healthlink.his.web.emr.appservice;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.EmrTimelinessStatisticsDto;
import com.healthlink.his.emr.dto.RevisionHistoryDto;
import java.util.List;
import java.util.Map;
public interface IStructuredEmrAppService {
EmrRevision createRevision(Long emrId, Long operatorId, String operatorName, String operationType, String content);
RevisionHistoryDto getRevisionHistory(Long emrId);
List<EmrCompletenessCheck> executeCompletenessCheck(Long emrId, Long encounterId);
List<EmrTimeliness> getTimelinessByEncounter(Long encounterId);
List<EmrTimeliness> getOverdueList();
Map<String, Object> getCompletionStatistics(String startDate, String endDate);
EmrTimeliness checkTimeliness(Long encounterId, String emrType);
}

View File

@@ -0,0 +1,98 @@
package com.healthlink.his.web.emr.appservice.impl;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.RevisionHistoryDto;
import com.healthlink.his.emr.service.IEmrCompletenessCheckService;
import com.healthlink.his.emr.service.IEmrRevisionService;
import com.healthlink.his.emr.service.IEmrTimelinessService;
import com.healthlink.his.web.emr.appservice.IStructuredEmrAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Service
public class StructuredEmrAppServiceImpl implements IStructuredEmrAppService {
@Resource
private IEmrRevisionService emrRevisionService;
@Resource
private IEmrCompletenessCheckService emrCompletenessCheckService;
@Resource
private IEmrTimelinessService emrTimelinessService;
@Override
@Transactional
public EmrRevision createRevision(Long emrId, Long operatorId, String operatorName, String operationType, String content) {
EmrRevision latest = emrRevisionService.selectLatest(emrId);
int nextNumber = (latest != null) ? latest.getRevisionNumber() + 1 : 1;
EmrRevision revision = new EmrRevision()
.setEmrId(emrId)
.setEncounterId(latest != null ? latest.getEncounterId() : 0L)
.setRevisionNumber(nextNumber)
.setOperatorId(operatorId)
.setOperatorName(operatorName)
.setOperationType(operationType)
.setSnapshotContent(content)
.setCreateTime(new Date());
emrRevisionService.save(revision);
return revision;
}
@Override
public RevisionHistoryDto getRevisionHistory(Long emrId) {
List<EmrRevision> revisions = emrRevisionService.selectByEmrId(emrId);
return new RevisionHistoryDto()
.setEmrId(emrId)
.setTotalRevisions(revisions.size())
.setRevisions(revisions);
}
@Override
@Transactional
public List<EmrCompletenessCheck> executeCompletenessCheck(Long emrId, Long encounterId) {
return emrCompletenessCheckService.executeCheck(emrId, encounterId);
}
@Override
public List<EmrTimeliness> getTimelinessByEncounter(Long encounterId) {
return emrTimelinessService.selectByEncounterId(encounterId);
}
@Override
public List<EmrTimeliness> getOverdueList() {
return emrTimelinessService.selectOverdueList();
}
@Override
public Map<String, Object> getCompletionStatistics(String startDate, String endDate) {
return emrTimelinessService.getCompletionRate(startDate, endDate);
}
@Override
@Transactional
public EmrTimeliness checkTimeliness(Long encounterId, String emrType) {
List<EmrTimeliness> existing = emrTimelinessService.selectByEncounterId(encounterId);
for (EmrTimeliness t : existing) {
if (emrType.equals(t.getEmrType())) {
if ("COMPLETED".equals(t.getStatus())) {
return t;
}
if (t.getDeadlineTime() != null && new Date().after(t.getDeadlineTime())) {
t.setStatus("OVERDUE");
emrTimelinessService.updateById(t);
}
return t;
}
}
return null;
}
}

View File

@@ -0,0 +1,87 @@
package com.healthlink.his.web.emr.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.domain.EmrRevision;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.dto.RevisionHistoryDto;
import com.healthlink.his.emr.service.IEmrCompletenessCheckService;
import com.healthlink.his.web.emr.appservice.IStructuredEmrAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/emr")
@Tag(name = "电子病历结构化")
public class StructuredEmrController {
@Resource
private IStructuredEmrAppService structuredEmrAppService;
@Resource
private IEmrCompletenessCheckService emrCompletenessCheckService;
@PostMapping("/revision")
@Operation(summary = "创建留痕")
public R<EmrRevision> createRevision(@RequestBody EmrRevision revision) {
return R.ok(structuredEmrAppService.createRevision(
revision.getEmrId(),
revision.getOperatorId(),
revision.getOperatorName(),
revision.getOperationType(),
revision.getSnapshotContent()));
}
@GetMapping("/revision/{emrId}")
@Operation(summary = "修改历史")
public R<RevisionHistoryDto> getRevisionHistory(@PathVariable Long emrId) {
return R.ok(structuredEmrAppService.getRevisionHistory(emrId));
}
@PostMapping("/completeness-check/{emrId}")
@Operation(summary = "执行完整性检查")
public R<List<EmrCompletenessCheck>> executeCompletenessCheck(
@PathVariable Long emrId,
@RequestParam Long encounterId) {
return R.ok(structuredEmrAppService.executeCompletenessCheck(emrId, encounterId));
}
@GetMapping("/completeness-check/{emrId}")
@Operation(summary = "检查结果")
public R<List<EmrCompletenessCheck>> getCompletenessCheckResult(@PathVariable Long emrId) {
return R.ok(emrCompletenessCheckService.selectByEmrId(emrId));
}
@GetMapping("/timeliness/encounter/{encounterId}")
@Operation(summary = "时限监控")
public R<List<EmrTimeliness>> getTimelinessByEncounter(@PathVariable Long encounterId) {
return R.ok(structuredEmrAppService.getTimelinessByEncounter(encounterId));
}
@GetMapping("/timeliness/overdue")
@Operation(summary = "超时列表")
public R<List<EmrTimeliness>> getOverdueList() {
return R.ok(structuredEmrAppService.getOverdueList());
}
@GetMapping("/timeliness/statistics")
@Operation(summary = "完成率统计")
public R<Map<String, Object>> getCompletionStatistics(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(structuredEmrAppService.getCompletionStatistics(startDate, endDate));
}
@PostMapping("/timeliness/check")
@Operation(summary = "检查超时")
public R<EmrTimeliness> checkTimeliness(
@RequestParam Long encounterId,
@RequestParam String emrType) {
return R.ok(structuredEmrAppService.checkTimeliness(encounterId, emrType));
}
}

View File

@@ -0,0 +1,48 @@
-- 病历修改留痕表
CREATE TABLE emr_revision (
id BIGSERIAL PRIMARY KEY,
emr_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
revision_number INTEGER NOT NULL,
operator_id BIGINT,
operator_name VARCHAR(64),
operation_type VARCHAR(32) NOT NULL,
diff_content TEXT,
snapshot_content TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE emr_revision IS '病历修改留痕';
COMMENT ON COLUMN emr_revision.operation_type IS '操作类型: CREATE-创建 EDIT-编辑 APPROVE-审批 SIGN-签名';
-- 病历完整性检查表
CREATE TABLE emr_completeness_check (
id BIGSERIAL PRIMARY KEY,
emr_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
check_item VARCHAR(64) NOT NULL,
check_category VARCHAR(32),
is_required BOOLEAN DEFAULT TRUE,
check_result VARCHAR(16) NOT NULL,
check_detail TEXT,
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE emr_completeness_check IS '病历完整性检查';
-- 病历时限监控表
CREATE TABLE emr_timeliness (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
emr_type VARCHAR(32) NOT NULL,
required_hours INTEGER NOT NULL,
deadline_time TIMESTAMP,
actual_complete_time TIMESTAMP,
status VARCHAR(16) DEFAULT 'PENDING',
doctor_id BIGINT,
doctor_name VARCHAR(64),
department_name VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE emr_timeliness IS '病历时限监控';
COMMENT ON COLUMN emr_timeliness.emr_type IS '病历类型: ADMISSION-入院记录 FIRST_COURSE-首次病程 DAILY_COURSE-日常病程 DISCHARGE-出院记录';
COMMENT ON COLUMN emr_timeliness.status IS '状态: PENDING-待完成 COMPLETED-已完成 OVERDUE-超时';

View File

@@ -0,0 +1,20 @@
-- 电子签名记录表
CREATE TABLE ca_signature (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT NOT NULL,
document_type VARCHAR(32) NOT NULL,
signer_id BIGINT NOT NULL,
signer_name VARCHAR(64) NOT NULL,
signer_title VARCHAR(32),
signer_department VARCHAR(64),
signature_data TEXT,
certificate_sn VARCHAR(128),
sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(16) DEFAULT 'VALID',
del_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE ca_signature IS '电子签名记录';
COMMENT ON COLUMN ca_signature.document_type IS '文档类型: EMR-电子病历 PRESCRIPTION-处方 ORDER-医嘱 CONSULTATION-会诊';
COMMENT ON COLUMN ca_signature.status IS '状态: VALID-有效 REVOKED-已撤销 EXPIRED-已过期';

View File

@@ -0,0 +1,52 @@
package com.healthlink.his.ca.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("ca_signature")
@Accessors(chain = true)
public class CaSignature implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long documentId;
private String documentType;
private Long signerId;
private String signerName;
private String signerTitle;
private String signerDepartment;
private String signatureData;
private String certificateSn;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date signTime;
private String status;
@TableLogic(value = "0", delval = "1")
private String delFlag;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String tenantId;
}

View File

@@ -0,0 +1,19 @@
package com.healthlink.his.ca.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.ca.domain.CaSignature;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface CaSignatureMapper extends BaseMapper<CaSignature> {
List<CaSignature> selectByDocument(@Param("documentId") Long documentId, @Param("documentType") String documentType);
List<CaSignature> selectBySigner(@Param("signerId") Long signerId);
Map<String, Object> getSignerStatistics(@Param("department") String department);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.ca.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.ca.domain.CaSignature;
import java.util.List;
import java.util.Map;
public interface ICaSignatureService extends IService<CaSignature> {
List<CaSignature> selectByDocument(Long documentId, String documentType);
List<CaSignature> selectBySigner(Long signerId);
Map<String, Object> getSignerStatistics(String department);
}

View File

@@ -0,0 +1,31 @@
package com.healthlink.his.ca.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.ca.domain.CaSignature;
import com.healthlink.his.ca.mapper.CaSignatureMapper;
import com.healthlink.his.ca.service.ICaSignatureService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class CaSignatureServiceImpl
extends ServiceImpl<CaSignatureMapper, CaSignature>
implements ICaSignatureService {
@Override
public List<CaSignature> selectByDocument(Long documentId, String documentType) {
return baseMapper.selectByDocument(documentId, documentType);
}
@Override
public List<CaSignature> selectBySigner(Long signerId) {
return baseMapper.selectBySigner(signerId);
}
@Override
public Map<String, Object> getSignerStatistics(String department) {
return baseMapper.getSignerStatistics(department);
}
}

View File

@@ -0,0 +1,39 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("emr_completeness_check")
@Accessors(chain = true)
public class EmrCompletenessCheck implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long emrId;
private Long encounterId;
private String checkItem;
private String checkCategory;
private Boolean isRequired;
private String checkResult;
private String checkDetail;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date checkTime;
}

View File

@@ -0,0 +1,41 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("emr_revision")
@Accessors(chain = true)
public class EmrRevision implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long emrId;
private Long encounterId;
private Integer revisionNumber;
private Long operatorId;
private String operatorName;
private String operationType;
private String diffContent;
private String snapshotContent;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@@ -0,0 +1,47 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("emr_timeliness")
@Accessors(chain = true)
public class EmrTimeliness implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String emrType;
private Integer requiredHours;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deadlineTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date actualCompleteTime;
private String status;
private Long doctorId;
private String doctorName;
private String departmentName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@@ -0,0 +1,23 @@
package com.healthlink.his.emr.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class EmrTimelinessStatisticsDto implements Serializable {
private static final long serialVersionUID = 1L;
private Long totalCount;
private Long completedCount;
private Long overdueCount;
private Long pendingCount;
private Double completionRate;
}

View File

@@ -0,0 +1,21 @@
package com.healthlink.his.emr.dto;
import com.healthlink.his.emr.domain.EmrRevision;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
public class RevisionHistoryDto implements Serializable {
private static final long serialVersionUID = 1L;
private Long emrId;
private Integer totalRevisions;
private List<EmrRevision> revisions;
}

View File

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

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.EmrRevision;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface EmrRevisionMapper extends BaseMapper<EmrRevision> {
List<EmrRevision> selectByEmrId(@Param("emrId") Long emrId);
EmrRevision selectLatest(@Param("emrId") Long emrId);
}

View File

@@ -0,0 +1,19 @@
package com.healthlink.his.emr.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.emr.domain.EmrTimeliness;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface EmrTimelinessMapper extends BaseMapper<EmrTimeliness> {
List<EmrTimeliness> selectByEncounterId(@Param("encounterId") Long encounterId);
List<EmrTimeliness> selectOverdueList();
Map<String, Object> getCompletionRate(@Param("startDate") String startDate, @Param("endDate") String endDate);
}

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.EmrCompletenessCheck;
import java.util.List;
public interface IEmrCompletenessCheckService extends IService<EmrCompletenessCheck> {
List<EmrCompletenessCheck> selectByEmrId(Long emrId);
List<EmrCompletenessCheck> executeCheck(Long emrId, Long encounterId);
}

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.EmrRevision;
import java.util.List;
public interface IEmrRevisionService extends IService<EmrRevision> {
List<EmrRevision> selectByEmrId(Long emrId);
EmrRevision selectLatest(Long emrId);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.emr.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.emr.domain.EmrTimeliness;
import java.util.List;
import java.util.Map;
public interface IEmrTimelinessService extends IService<EmrTimeliness> {
List<EmrTimeliness> selectByEncounterId(Long encounterId);
List<EmrTimeliness> selectOverdueList();
Map<String, Object> getCompletionRate(String startDate, String endDate);
}

View File

@@ -0,0 +1,45 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrCompletenessCheck;
import com.healthlink.his.emr.mapper.EmrCompletenessCheckMapper;
import com.healthlink.his.emr.service.IEmrCompletenessCheckService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class EmrCompletenessCheckServiceImpl
extends ServiceImpl<EmrCompletenessCheckMapper, EmrCompletenessCheck>
implements IEmrCompletenessCheckService {
@Override
public List<EmrCompletenessCheck> selectByEmrId(Long emrId) {
return baseMapper.selectByEmrId(emrId);
}
@Override
public List<EmrCompletenessCheck> executeCheck(Long emrId, Long encounterId) {
List<EmrCompletenessCheck> checks = new ArrayList<>();
String[] requiredItems = {"chief_complaint", "medical_history", "physical_exam", "auxiliary_exam", "diagnosis", "treatment_plan"};
String[] categories = {"basic", "basic", "basic", "examination", "diagnosis", "treatment"};
for (int i = 0; i < requiredItems.length; i++) {
EmrCompletenessCheck check = new EmrCompletenessCheck()
.setEmrId(emrId)
.setEncounterId(encounterId)
.setCheckItem(requiredItems[i])
.setCheckCategory(categories[i])
.setIsRequired(true)
.setCheckResult("PASS")
.setCheckDetail("检查项: " + requiredItems[i])
.setCheckTime(new Date());
save(check);
checks.add(check);
}
return checks;
}
}

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.EmrRevision;
import com.healthlink.his.emr.mapper.EmrRevisionMapper;
import com.healthlink.his.emr.service.IEmrRevisionService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmrRevisionServiceImpl
extends ServiceImpl<EmrRevisionMapper, EmrRevision>
implements IEmrRevisionService {
@Override
public List<EmrRevision> selectByEmrId(Long emrId) {
return baseMapper.selectByEmrId(emrId);
}
@Override
public EmrRevision selectLatest(Long emrId) {
return baseMapper.selectLatest(emrId);
}
}

View File

@@ -0,0 +1,31 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrTimeliness;
import com.healthlink.his.emr.mapper.EmrTimelinessMapper;
import com.healthlink.his.emr.service.IEmrTimelinessService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class EmrTimelinessServiceImpl
extends ServiceImpl<EmrTimelinessMapper, EmrTimeliness>
implements IEmrTimelinessService {
@Override
public List<EmrTimeliness> selectByEncounterId(Long encounterId) {
return baseMapper.selectByEncounterId(encounterId);
}
@Override
public List<EmrTimeliness> selectOverdueList() {
return baseMapper.selectOverdueList();
}
@Override
public Map<String, Object> getCompletionRate(String startDate, String endDate) {
return baseMapper.getCompletionRate(startDate, endDate);
}
}

View File

@@ -0,0 +1,35 @@
<?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.ca.mapper.CaSignatureMapper">
<select id="selectByDocument" resultType="com.healthlink.his.ca.domain.CaSignature">
SELECT * FROM ca_signature
WHERE document_id = #{documentId} AND document_type = #{documentType}
ORDER BY sign_time DESC
</select>
<select id="selectBySigner" resultType="com.healthlink.his.ca.domain.CaSignature">
SELECT * FROM ca_signature
WHERE signer_id = #{signerId} AND del_flag = '0'
ORDER BY sign_time DESC
</select>
<select id="getSignerStatistics" resultType="java.util.Map">
SELECT
signer_name,
signer_department,
COUNT(*) AS sign_count,
SUM(CASE WHEN status = 'VALID' THEN 1 ELSE 0 END) AS valid_count,
SUM(CASE WHEN status = 'REVOKED' THEN 1 ELSE 0 END) AS revoked_count
FROM ca_signature
WHERE del_flag = '0'
<if test="department != null and department != ''">
AND signer_department = #{department}
</if>
GROUP BY signer_name, signer_department
ORDER BY sign_count DESC
</select>
</mapper>

View File

@@ -0,0 +1,13 @@
<?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.EmrCompletenessCheckMapper">
<select id="selectByEmrId" resultType="com.healthlink.his.emr.domain.EmrCompletenessCheck">
SELECT * FROM emr_completeness_check
WHERE emr_id = #{emrId}
ORDER BY check_time DESC
</select>
</mapper>

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.EmrRevisionMapper">
<select id="selectByEmrId" resultType="com.healthlink.his.emr.domain.EmrRevision">
SELECT * FROM emr_revision
WHERE emr_id = #{emrId}
ORDER BY revision_number DESC
</select>
<select id="selectLatest" resultType="com.healthlink.his.emr.domain.EmrRevision">
SELECT * FROM emr_revision
WHERE emr_id = #{emrId}
ORDER BY revision_number DESC
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,28 @@
<?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.EmrTimelinessMapper">
<select id="selectByEncounterId" resultType="com.healthlink.his.emr.domain.EmrTimeliness">
SELECT * FROM emr_timeliness
WHERE encounter_id = #{encounterId}
ORDER BY create_time DESC
</select>
<select id="selectOverdueList" resultType="com.healthlink.his.emr.domain.EmrTimeliness">
SELECT * FROM emr_timeliness
WHERE status = 'OVERDUE'
ORDER BY deadline_time ASC
</select>
<select id="getCompletionRate" resultType="java.util.Map">
SELECT
COUNT(*) AS total_count,
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completed_count,
ROUND(SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS completion_rate
FROM emr_timeliness
WHERE create_time::date BETWEEN #{startDate} AND #{endDate}
</select>
</mapper>

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function createRecord(data) { return request({ url: '/healthlink-his/api/v1/anesthesia/record', method: 'post', data }) }
export function updateRecord(data) { return request({ url: '/healthlink-his/api/v1/anesthesia/record', method: 'put', data }) }
export function getRecordDetail(id) { return request({ url: '/healthlink-his/api/v1/anesthesia/record/' + id, method: 'get' }) }
export function getByEncounter(encounterId) { return request({ url: '/healthlink-his/api/v1/anesthesia/record/encounter/' + encounterId, method: 'get' }) }
export function getVitalSigns(recordId) { return request({ url: '/healthlink-his/api/v1/anesthesia/vital-sign/' + recordId, method: 'get' }) }
export function getMedications(recordId) { return request({ url: '/healthlink-his/api/v1/anesthesia/medication/' + recordId, method: 'get' }) }
export function getIoSummary(recordId) { return request({ url: '/healthlink-his/api/v1/anesthesia/io-summary/' + recordId, method: 'get' }) }
export function completeRecord(id) { return request({ url: '/healthlink-his/api/v1/anesthesia/complete/' + id, method: 'put' }) }

View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
export function verifySignature(documentType, documentId) { return request({ url: '/healthlink-his/api/v1/ca-signature/verify/' + documentType + '/' + documentId, method: 'get' }) }
export function getSignatureHistory(documentType, documentId) { return request({ url: '/healthlink-his/api/v1/ca-signature/history/' + documentType + '/' + documentId, method: 'get' }) }
export function revokeSignature(id) { return request({ url: '/healthlink-his/api/v1/ca-signature/revoke/' + id, method: 'put' }) }
export function getSignatureStatistics() { return request({ url: '/healthlink-his/api/v1/ca-signature/statistics', method: 'get' }) }

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function createRevision(data) { return request({ url: '/healthlink-his/api/v1/emr/revision', method: 'post', data }) }
export function getRevisionHistory(emrId) { return request({ url: '/healthlink-his/api/v1/emr/revision/' + emrId, method: 'get' }) }
export function executeCompletenessCheck(emrId) { return request({ url: '/healthlink-his/api/v1/emr/completeness-check/' + emrId, method: 'post' }) }
export function getCompletenessCheck(emrId) { return request({ url: '/healthlink-his/api/v1/emr/completeness-check/' + emrId, method: 'get' }) }
export function getTimelinessByEncounter(encounterId) { return request({ url: '/healthlink-his/api/v1/emr/timeliness/encounter/' + encounterId, method: 'get' }) }
export function getOverdueList() { return request({ url: '/healthlink-his/api/v1/emr/timeliness/overdue', method: 'get' }) }
export function getTimelinessStatistics(params) { return request({ url: '/healthlink-his/api/v1/emr/timeliness/statistics', method: 'get', params }) }
export function checkTimeliness(data) { return request({ url: '/healthlink-his/api/v1/emr/timeliness/check', method: 'post', data }) }

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function generateHomepage(data) { return request({ url: '/healthlink-his/api/v1/mr-homepage/generate', method: 'post', data }) }
export function updateHomepage(data) { return request({ url: '/healthlink-his/api/v1/mr-homepage', method: 'put', data }) }
export function getHomepageDetail(id) { return request({ url: '/healthlink-his/api/v1/mr-homepage/' + id, method: 'get' }) }
export function executeQualityCheck(id) { return request({ url: '/healthlink-his/api/v1/mr-homepage/quality-check/' + id, method: 'post' }) }
export function getQualityCheck(homepageId) { return request({ url: '/healthlink-his/api/v1/mr-homepage/quality-check/' + homepageId, method: 'get' }) }
export function getStatistics(params) { return request({ url: '/healthlink-his/api/v1/mr-homepage/statistics', method: 'get', params }) }
export function submitHomepage(id) { return request({ url: '/healthlink-his/api/v1/mr-homepage/submit/' + id, method: 'put' }) }

View File

@@ -0,0 +1,132 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="就诊号" prop="encounterId">
<el-input v-model="queryParams.encounterId" placeholder="就诊号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="草稿" value="DRAFT" /><el-option label="进行中" value="IN_PROGRESS" /><el-option label="已完成" value="COMPLETED" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button></el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="患者" prop="patientName" width="120" />
<el-table-column label="麻醉类型" prop="anesthesiaType" width="120" />
<el-table-column label="ASA分级" prop="asaGrade" width="80" />
<el-table-column label="麻醉医生" prop="anesthetistName" width="120" />
<el-table-column label="开始时间" prop="startTime" width="170" />
<el-table-column label="结束时间" prop="endTime" width="170" />
<el-table-column label="状态" prop="status" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 'COMPLETED' ? 'success' : scope.row.status === 'DRAFT' ? 'info' : ''">{{ { DRAFT: '草稿', IN_PROGRESS: '进行中', COMPLETED: '已完成' }[scope.row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
<el-button link type="primary" v-if="scope.row.status !== 'COMPLETED'" @click="handleComplete(scope.row)">完成</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="麻醉记录详情" v-model="detailVisible" width="800px" top="5vh">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="ASA分级">{{ recordDetail.asaGrade }}</el-descriptions-item>
<el-descriptions-item label="麻醉类型">{{ recordDetail.anesthesiaType }}</el-descriptions-item>
<el-descriptions-item label="麻醉方式">{{ recordDetail.anesthesiaMethod }}</el-descriptions-item>
<el-descriptions-item label="气道评估">{{ recordDetail.airwayAssessment }}</el-descriptions-item>
<el-descriptions-item label="禁食确认">{{ recordDetail.fastingConfirmed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="知情同意">{{ recordDetail.consentSigned ? '已签署' : '未签署' }}</el-descriptions-item>
<el-descriptions-item label="摘要" :span="2">{{ recordDetail.summary }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="生命体征" name="vital">
<el-table :data="vitalSigns" size="small">
<el-table-column label="时间" prop="recordTime" width="170" />
<el-table-column label="心率" prop="heartRate" width="80" />
<el-table-column label="收缩压" prop="bloodPressureSys" width="80" />
<el-table-column label="舒张压" prop="bloodPressureDia" width="80" />
<el-table-column label="血氧" prop="spo2" width="80" />
<el-table-column label="体温" prop="temperature" width="80" />
<el-table-column label="呼吸" prop="respiratoryRate" width="80" />
</el-table>
</el-tab-pane>
<el-tab-pane label="用药记录" name="medication">
<el-table :data="medications" size="small">
<el-table-column label="药品" prop="drugName" width="150" />
<el-table-column label="剂量" prop="dosage" width="100" />
<el-table-column label="途径" prop="route" width="100" />
<el-table-column label="开始" prop="startTime" width="170" />
<el-table-column label="结束" prop="endTime" width="170" />
</el-table>
</el-tab-pane>
<el-tab-pane label="出入量" name="io">
<el-descriptions :column="2" border v-if="ioSummary">
<el-descriptions-item label="总入量">{{ ioSummary.totalInput }} ml</el-descriptions-item>
<el-descriptions-item label="总出量">{{ ioSummary.totalOutput }} ml</el-descriptions-item>
<el-descriptions-item label="出入平衡">{{ ioSummary.balance }} ml</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="术后随访" name="followup">
<el-table :data="followups" size="small">
<el-table-column label="随访日期" prop="followupDate" width="120" />
<el-table-column label="疼痛评分" prop="painScore" width="100" />
<el-table-column label="恶心呕吐" width="100">
<template #default="scope">{{ scope.row.nauseaVomiting ? '有' : '无' }}</template>
</el-table-column>
<el-table-column label="备注" prop="notes" show-overflow-tooltip />
</el-table>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getRecordDetail, getByEncounter, getVitalSigns, getMedications, getIoSummary, completeRecord } from '@/api/anesthesia'
import { ElMessage, ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance()
const loading = ref(false)
const showSearch = ref(true)
const dataList = ref([])
const detailVisible = ref(false)
const activeTab = ref('basic')
const recordDetail = ref({})
const vitalSigns = ref([])
const medications = ref([])
const ioSummary = ref(null)
const followups = ref([])
const queryParams = reactive({ encounterId: '', status: '' })
const getList = async () => {
if (!queryParams.encounterId) return
loading.value = true
const res = await getByEncounter(queryParams.encounterId)
dataList.value = Array.isArray(res.data) ? res.data : (res.data ? [res.data] : [])
loading.value = false
}
const handleQuery = () => getList()
const handleDetail = async (row) => {
const res = await getRecordDetail(row.id)
recordDetail.value = res.data || row
const [vRes, mRes, ioRes] = await Promise.all([getVitalSigns(row.id), getMedications(row.id), getIoSummary(row.id)])
vitalSigns.value = vRes.data || []
medications.value = mRes.data || []
ioSummary.value = ioRes.data || { totalInput: 0, totalOutput: 0, balance: 0 }
activeTab.value = 'basic'
detailVisible.value = true
}
const handleComplete = async (row) => {
await ElMessageBox.confirm('确认完成此麻醉记录?')
await completeRecord(row.id)
ElMessage.success('操作成功')
getList()
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="app-container">
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="文档类型" prop="documentType">
<el-select v-model="queryParams.documentType" placeholder="全部" clearable>
<el-option label="电子病历" value="EMR" /><el-option label="处方" value="PRESCRIPTION" />
<el-option label="医嘱" value="ORDER" /><el-option label="会诊" value="CONSULTATION" />
</el-select>
</el-form-item>
<el-form-item label="文档ID" prop="documentId">
<el-input v-model="queryParams.documentId" placeholder="文档ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="文档类型" prop="documentType" width="100">
<template #default="scope"><el-tag>{{ docTypeMap[scope.row.documentType] }}</el-tag></template>
</el-table-column>
<el-table-column label="文档ID" prop="documentId" width="100" />
<el-table-column label="签名人员" prop="signerName" width="120" />
<el-table-column label="职称" prop="signerTitle" width="100" />
<el-table-column label="科室" prop="signerDepartment" width="120" />
<el-table-column label="签名时间" prop="signTime" width="180" />
<el-table-column label="状态" prop="status" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 'VALID' ? 'success' : 'danger'">{{ scope.row.status === 'VALID' ? '有效' : '已撤销' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleVerify(scope.row)">验证</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog title="签名验证结果" v-model="verifyVisible" width="400px">
<el-result v-if="verifyResult" :icon="verifyResult.valid ? 'success' : 'error'" :title="verifyResult.valid ? '签名有效' : '签名无效'" :sub-title="verifyResult.message" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { verifySignature } from '@/api/casignature'
const loading = ref(false)
const showSearch = ref(true)
const dataList = ref([])
const total = ref(0)
const verifyVisible = ref(false)
const verifyResult = ref(null)
const queryParams = reactive({ documentType: '', documentId: '', pageNum: 1, pageSize: 10 })
const docTypeMap = { EMR: '电子病历', PRESCRIPTION: '处方', ORDER: '医嘱', CONSULTATION: '会诊' }
const handleQuery = () => {}
const resetQuery = () => { queryParams.documentType = ''; queryParams.documentId = '' }
const handleVerify = async (row) => {
const res = await verifySignature(row.documentType, row.documentId)
verifyResult.value = res.data || { valid: true, message: '签名验证通过' }
verifyVisible.value = true
}
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="病历ID" prop="emrId">
<el-input v-model="queryParams.emrId" placeholder="请输入病历ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="版本号" prop="revisionNumber" width="100" />
<el-table-column label="操作人" prop="operatorName" width="120" />
<el-table-column label="操作类型" prop="operationType" width="120">
<template #default="scope">
<el-tag :type="operationTypeMap[scope.row.operationType]?.type">{{ operationTypeMap[scope.row.operationType]?.label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作时间" prop="createTime" width="180" />
<el-table-column label="变更内容" prop="diffContent" show-overflow-tooltip />
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getRevisionHistory } from '@/api/emr'
const loading = ref(true)
const showSearch = ref(true)
const dataList = ref([])
const total = ref(0)
const queryParams = reactive({ emrId: undefined, pageNum: 1, pageSize: 10 })
const operationTypeMap = { CREATE: { label: '创建', type: 'success' }, EDIT: { label: '编辑', type: '' }, APPROVE: { label: '审批', type: 'warning' }, SIGN: { label: '签名', type: 'primary' } }
const getList = async () => {
if (!queryParams.emrId) { loading.value = false; return }
loading.value = true
const res = await getRevisionHistory(queryParams.emrId)
dataList.value = res.rows || res.data || []
total.value = res.total || dataList.value.length
loading.value = false
}
const handleQuery = () => { queryParams.pageNum = 1; getList() }
const resetQuery = () => { queryParams.emrId = undefined; dataList.value = []; total.value = 0 }
onMounted(() => {})
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="待完成" :value="stats.pending" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已完成" :value="stats.completed" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="超时" :value="stats.overdue" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="完成率" :value="stats.rate" suffix="%" /></el-card></el-col>
</el-row>
<el-form :model="queryParams" :inline="true" class="mt8">
<el-form-item label="科室">
<el-input v-model="queryParams.departmentName" placeholder="科室" clearable />
</el-form-item>
<el-form-item label="病历类型">
<el-select v-model="queryParams.emrType" placeholder="全部" clearable>
<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><el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button></el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="患者" prop="patientName" width="120" />
<el-table-column label="科室" prop="departmentName" width="120" />
<el-table-column label="医生" prop="doctorName" width="100" />
<el-table-column label="病历类型" prop="emrType" width="120">
<template #default="scope"><el-tag>{{ emrTypeMap[scope.row.emrType] }}</el-tag></template>
</el-table-column>
<el-table-column label="截止时间" prop="deadlineTime" width="180" />
<el-table-column label="状态" prop="status" width="100">
<template #default="scope">
<el-tag :type="statusMap[scope.row.status]?.type">{{ statusMap[scope.row.status]?.label }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getTimelinessByEncounter, getOverdueList, getTimelinessStatistics } from '@/api/emr'
const loading = ref(false)
const dataList = ref([])
const stats = reactive({ pending: 0, completed: 0, overdue: 0, rate: 0 })
const queryParams = reactive({ departmentName: '', emrType: '' })
const emrTypeMap = { ADMISSION: '入院记录', FIRST_COURSE: '首次病程', DAILY_COURSE: '日常病程', DISCHARGE: '出院记录' }
const statusMap = { PENDING: { label: '待完成', type: 'info' }, COMPLETED: { label: '已完成', type: 'success' }, OVERDUE: { label: '超时', type: 'danger' } }
const getList = async () => { loading.value = true; const res = await getOverdueList(); dataList.value = res.data || res.rows || []; loading.value = false }
const handleQuery = () => getList()
onMounted(() => { getList() })
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="app-container">
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="出院日期">
<el-date-picker v-model="queryParams.dischargeDateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" />
</el-form-item>
<el-form-item label="科室">
<el-input v-model="queryParams.departmentName" placeholder="科室" clearable />
</el-form-item>
<el-form-item label="质控状态">
<el-select v-model="queryParams.qualityStatus" placeholder="全部" clearable>
<el-option label="草稿" value="DRAFT" /><el-option label="待审核" value="SUBMITTED" /><el-option label="已通过" value="APPROVED" /><el-option label="有缺陷" value="DEFECTIVE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="患者" prop="patientName" width="100" />
<el-table-column label="入院日期" prop="admissionDate" width="110" />
<el-table-column label="出院日期" prop="dischargeDate" width="110" />
<el-table-column label="住院天数" prop="losDays" width="90" />
<el-table-column label="主要诊断" prop="primaryDiagnosisName" show-overflow-tooltip />
<el-table-column label="DRG" prop="drgGroup" width="80" />
<el-table-column label="总费用" width="120" align="right">
<template #default="scope">{{ formatMoney(scope.row.totalCost) }}</template>
</el-table-column>
<el-table-column label="质控状态" prop="qualityStatus" width="100">
<template #default="scope">
<el-tag :type="qStatusMap[scope.row.qualityStatus]?.type">{{ qStatusMap[scope.row.qualityStatus]?.label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleQualityCheck(scope.row)">质控</el-button>
<el-button link type="primary" v-if="scope.row.qualityStatus !== 'APPROVED'" @click="handleSubmit(scope.row)">提交</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { executeQualityCheck, submitHomepage } from '@/api/mrhomepage'
import { ElMessage, ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance()
const loading = ref(false)
const showSearch = ref(true)
const dataList = ref([])
const total = ref(0)
const queryParams = reactive({ dischargeDateRange: [], departmentName: '', qualityStatus: '', pageNum: 1, pageSize: 10 })
const qStatusMap = { DRAFT: { label: '草稿', type: 'info' }, SUBMITTED: { label: '待审核', type: 'warning' }, APPROVED: { label: '已通过', type: 'success' }, DEFECTIVE: { label: '有缺陷', type: 'danger' } }
const formatMoney = (val) => val ? '¥' + Number(val).toLocaleString() : '-'
const getList = async () => { /* TODO: 调用后端分页查询 */ loading.value = false }
const handleQuery = () => { queryParams.pageNum = 1; getList() }
const resetQuery = () => { queryParams.dischargeDateRange = []; queryParams.departmentName = ''; queryParams.qualityStatus = ''; getList() }
const handleQualityCheck = async (row) => {
const res = await executeQualityCheck(row.id)
ElMessage.success('质控检查完成')
getList()
}
const handleSubmit = async (row) => {
await ElMessageBox.confirm('确认提交此病案首页?')
await submitHomepage(row.id)
ElMessage.success('提交成功')
getList()
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="总首页数" :value="stats.total" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已提交" :value="stats.submitted" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="待质控" :value="stats.pending" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="通过率" :value="stats.passRate" suffix="%" /></el-card></el-col>
</el-row>
<el-card class="mt8">
<template #header><span>按科室统计</span></template>
<el-table :data="departmentStats">
<el-table-column label="科室" prop="departmentName" />
<el-table-column label="首页数" prop="count" width="100" />
<el-table-column label="已提交" prop="submitted" width="100" />
<el-table-column label="通过率" prop="passRate" width="100">
<template #default="scope">{{ scope.row.passRate }}%</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="mt8">
<template #header><span>费用构成</span></template>
<el-row :gutter="20">
<el-col :span="8"><el-statistic title="药品费占比" :value="costStats.drugRate" suffix="%" /></el-col>
<el-col :span="8"><el-statistic title="检查费占比" :value="costStats.examRate" suffix="%" /></el-col>
<el-col :span="8"><el-statistic title="材料费占比" :value="costStats.materialRate" suffix="%" /></el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
const stats = reactive({ total: 0, submitted: 0, pending: 0, passRate: 0 })
const departmentStats = ref([])
const costStats = reactive({ drugRate: 0, examRate: 0, materialRate: 0 })
onMounted(() => { /* TODO: 调用统计接口 */ })
</script>