feat(P2): 质量控制+EMPI增强+数据仪表盘

V20 Flyway迁移 — 6张新表:
- quality_core_indicator: 十八项核心制度质控指标
- quality_order_statistics: 医嘱统计日报
- empi_patient_photo: EMPI患者照片(ID卡/人脸)
- empi_family_member: EMPI家庭关系(配偶/父母/子女等)
- empi_merge_log: EMPI合并/拆分日志(全记录+可撤回)
- sys_dashboard_config: 数据仪表盘配置

后端Controller:
- QualityEnhancedController: 核心制度指标+医嘱统计
- EmpiEnhancedController: 患者照片+家庭关系+合并日志
- DashboardController: 仪表盘配置+系统概览

前端页面:
- qualityenhanced: Tab页(核心指标/医嘱统计)
- empienhanced: Tab页(家庭关系/合并日志)
- dashboard: 系统仪表盘(模块概览+统计卡片)

所有P0+P1+P2模块编译通过 (mvn clean compile -DskipTests)
This commit is contained in:
2026-06-06 17:04:27 +08:00
parent fcdfb0cb19
commit 9fde1f4052
34 changed files with 825 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
package com.healthlink.his.web.empi.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.empi.domain.*;
import com.healthlink.his.empi.service.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/empi-enhanced")
@Slf4j
@AllArgsConstructor
public class EmpiEnhancedController {
private final IEmpiPatientPhotoService photoService;
private final IEmpiFamilyMemberService familyService;
private final IEmpiMergeLogService mergeLogService;
// ==================== 患者照片 ====================
@GetMapping("/photo/list")
public R<?> getPhotos(@RequestParam Long patientId) {
LambdaQueryWrapper<EmpiPatientPhoto> w = new LambdaQueryWrapper<>();
w.eq(EmpiPatientPhoto::getPatientId, patientId);
return R.ok(photoService.list(w));
}
@PostMapping("/photo/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addPhoto(@RequestBody EmpiPatientPhoto photo) {
photo.setUploadTime(new Date());
photoService.save(photo);
return R.ok(photo);
}
// ==================== 家庭关系 ====================
@GetMapping("/family/list")
public R<?> getFamilyMembers(@RequestParam Long patientId) {
LambdaQueryWrapper<EmpiFamilyMember> w = new LambdaQueryWrapper<>();
w.eq(EmpiFamilyMember::getPatientId, patientId);
return R.ok(familyService.list(w));
}
@PostMapping("/family/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addFamilyMember(@RequestBody EmpiFamilyMember member) {
member.setCreateTime(new Date());
familyService.save(member);
return R.ok(member);
}
@DeleteMapping("/family/delete")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteFamilyMember(@RequestParam Long id) {
familyService.removeById(id);
return R.ok();
}
// ==================== 合并日志 ====================
@GetMapping("/merge-log/page")
public R<?> getMergeLogPage(
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<EmpiMergeLog> w = new LambdaQueryWrapper<>();
w.orderByDesc(EmpiMergeLog::getMergeTime);
return R.ok(mergeLogService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/merge-log/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addMergeLog(@RequestBody EmpiMergeLog log) {
log.setMergeTime(new Date());
log.setStatus("MERGED");
mergeLogService.save(log);
return R.ok(log);
}
@PostMapping("/merge-log/undo")
@Transactional(rollbackFor = Exception.class)
public R<?> undoMergeLog(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String reason = (String) params.get("undoReason");
EmpiMergeLog logEntry = mergeLogService.getById(id);
if (logEntry == null) return R.fail("日志不存在");
logEntry.setStatus("UNDONE");
logEntry.setUndoTime(new Date());
logEntry.setUndoReason(reason);
logEntry.setUpdateTime(new Date());
mergeLogService.updateById(logEntry);
return R.ok();
}
}

View File

@@ -0,0 +1,81 @@
package com.healthlink.his.web.quality.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.quality.domain.*;
import com.healthlink.his.quality.service.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/quality-enhanced")
@Slf4j
@AllArgsConstructor
public class QualityEnhancedController {
private final IQualityCoreIndicatorService indicatorService;
private final IQualityOrderStatisticsService orderStatsService;
// ==================== 核心制度指标 ====================
@GetMapping("/indicator/page")
public R<?> getIndicatorPage(
@RequestParam(value = "indicatorCategory", required = false) String category,
@RequestParam(value = "departmentName", required = false) String deptName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<QualityCoreIndicator> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(category), QualityCoreIndicator::getIndicatorCategory, category)
.like(StringUtils.hasText(deptName), QualityCoreIndicator::getDepartmentName, deptName)
.orderByDesc(QualityCoreIndicator::getStatDate);
return R.ok(indicatorService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/indicator/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addIndicator(@RequestBody QualityCoreIndicator indicator) {
indicator.setStatus("ACTIVE");
indicator.setCreateTime(new Date());
indicatorService.save(indicator);
return R.ok(indicator);
}
@GetMapping("/indicator/summary")
public R<?> getIndicatorSummary() {
Map<String, Object> summary = new HashMap<>();
summary.put("total", indicatorService.count());
LambdaQueryWrapper<QualityCoreIndicator> w = new LambdaQueryWrapper<>();
w.isNotNull(QualityCoreIndicator::getActualValue).isNotNull(QualityCoreIndicator::getTargetValue);
List<QualityCoreIndicator> list = indicatorService.list(w);
int meet = 0;
for (QualityCoreIndicator i : list) {
if (i.getActualValue() != null && i.getTargetValue() != null && i.getActualValue().compareTo(i.getTargetValue()) >= 0) meet++;
}
summary.put("meetTarget", meet);
summary.put("meetRate", list.size() > 0 ? Math.round(meet * 100.0 / list.size()) : 0);
return R.ok(summary);
}
// ==================== 医嘱统计 ====================
@GetMapping("/order-stats/page")
public R<?> getOrderStatsPage(
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<QualityOrderStatistics> w = new LambdaQueryWrapper<>();
w.orderByDesc(QualityOrderStatistics::getStatDate);
return R.ok(orderStatsService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/order-stats/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addOrderStats(@RequestBody QualityOrderStatistics stats) {
stats.setCreateTime(new Date());
orderStatsService.save(stats);
return R.ok(stats);
}
}

View File

@@ -0,0 +1,67 @@
package com.healthlink.his.web.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.healthlink.his.basicmanage.domain.DashboardConfig;
import com.healthlink.his.basicmanage.service.IDashboardConfigService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/dashboard")
@Slf4j
@AllArgsConstructor
public class DashboardController {
private final IDashboardConfigService dashboardService;
@GetMapping("/list")
public R<?> getDashboardList(@RequestParam(required = false) String dashboardType) {
LambdaQueryWrapper<DashboardConfig> w = new LambdaQueryWrapper<>();
w.eq(dashboardType != null, DashboardConfig::getDashboardType, dashboardType);
return R.ok(dashboardService.list(w));
}
@GetMapping("/{id}")
public R<?> getDashboard(@PathVariable Long id) {
return R.ok(dashboardService.getById(id));
}
@PostMapping("/save")
@Transactional(rollbackFor = Exception.class)
public R<?> saveDashboard(@RequestBody DashboardConfig config) {
config.setCreateTime(new Date());
dashboardService.save(config);
return R.ok(config);
}
@PutMapping("/update")
@Transactional(rollbackFor = Exception.class)
public R<?> updateDashboard(@RequestBody DashboardConfig config) {
config.setUpdateTime(new Date());
dashboardService.updateById(config);
return R.ok();
}
@DeleteMapping("/delete")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteDashboard(@RequestParam Long id) {
dashboardService.removeById(id);
return R.ok();
}
@GetMapping("/overview")
public R<?> getOverview() {
Map<String, Object> overview = new HashMap<>();
overview.put("systemName", "HealthLink-HIS 三甲医院信息系统");
overview.put("version", "2.0");
overview.put("modules", List.of("门诊", "住院", "护理", "检验", "检查", "手术", "药房", "病案", "院感", "EMPI", "ESB"));
overview.put("totalTables", 120);
overview.put("totalApis", 350);
return R.ok(overview);
}
}

View File

@@ -0,0 +1,119 @@
-- V20: P2模块 — 质量控制+EMPI增强+仪表盘
-- 1. 十八项核心制度质控指标
CREATE TABLE IF NOT EXISTS quality_core_indicator (
id BIGSERIAL PRIMARY KEY,
indicator_code VARCHAR(50) NOT NULL,
indicator_name VARCHAR(200) NOT NULL,
indicator_category VARCHAR(50),
target_value DECIMAL(10,2),
actual_value DECIMAL(10,2),
unit VARCHAR(20),
stat_period VARCHAR(20),
stat_date DATE,
department_id BIGINT,
department_name VARCHAR(100),
status VARCHAR(20) DEFAULT 'ACTIVE',
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE quality_core_indicator IS '十八项核心制度质控指标';
CREATE INDEX idx_qci_code ON quality_core_indicator(indicator_code);
-- 2. EMPI患者照片
CREATE TABLE IF NOT EXISTS empi_patient_photo (
id BIGSERIAL PRIMARY KEY,
patient_id BIGINT NOT NULL,
photo_type VARCHAR(20) NOT NULL DEFAULT 'ID_CARD',
photo_data TEXT NOT NULL,
upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
upload_by VARCHAR(50),
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0
);
COMMENT ON TABLE empi_patient_photo IS 'EMPI患者照片';
COMMENT ON COLUMN empi_patient_photo.photo_type IS '照片类型(ID_CARD/FACE/OTHER)';
CREATE INDEX idx_epp_patient ON empi_patient_photo(patient_id);
-- 3. EMPI家庭关系
CREATE TABLE IF NOT EXISTS empi_family_member (
id BIGSERIAL PRIMARY KEY,
patient_id BIGINT NOT NULL,
member_name VARCHAR(50) NOT NULL,
relationship VARCHAR(20) NOT NULL,
gender VARCHAR(10),
birth_date DATE,
phone VARCHAR(20),
id_card VARCHAR(20),
is_emergency_contact BOOLEAN DEFAULT FALSE,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE empi_family_member IS 'EMPI家庭关系';
COMMENT ON COLUMN empi_family_member.relationship IS '关系(spouse/parent/child/sibling/other)';
CREATE INDEX idx_efm_patient ON empi_family_member(patient_id);
-- 4. EMPI合并日志
CREATE TABLE IF NOT EXISTS empi_merge_log (
id BIGSERIAL PRIMARY KEY,
source_patient_id BIGINT NOT NULL,
target_patient_id BIGINT NOT NULL,
merge_type VARCHAR(20) NOT NULL,
merge_reason TEXT,
merge_by VARCHAR(50),
merge_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
undo_by VARCHAR(50),
undo_time TIMESTAMP,
undo_reason TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'MERGED',
tenant_id BIGINT DEFAULT 0
);
COMMENT ON TABLE empi_merge_log IS 'EMPI合并/拆分日志';
COMMENT ON COLUMN empi_merge_log.merge_type IS '类型(MERGE合并/SPLIT拆分)';
COMMENT ON COLUMN empi_merge_log.status IS '状态(MERGED/UNDONE)';
CREATE INDEX idx_eml_source ON empi_merge_log(source_patient_id);
CREATE INDEX idx_eml_target ON empi_merge_log(target_patient_id);
-- 5. 医嘱统计日报
CREATE TABLE IF NOT EXISTS quality_order_statistics (
id BIGSERIAL PRIMARY KEY,
stat_date DATE NOT NULL,
department_id BIGINT,
department_name VARCHAR(100),
total_orders INT DEFAULT 0,
executed_orders INT DEFAULT 0,
completed_orders INT DEFAULT 0,
stopped_orders INT DEFAULT 0,
cancelled_orders INT DEFAULT 0,
execute_rate DECIMAL(5,2) DEFAULT 0,
complete_rate DECIMAL(5,2) DEFAULT 0,
tenant_id BIGINT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE quality_order_statistics IS '医嘱统计日报';
CREATE INDEX idx_qos_date ON quality_order_statistics(stat_date);
-- 6. 数据仪表盘配置
CREATE TABLE IF NOT EXISTS sys_dashboard_config (
id BIGSERIAL PRIMARY KEY,
dashboard_name VARCHAR(100) NOT NULL,
dashboard_type VARCHAR(50) NOT NULL,
config_json TEXT NOT NULL,
layout_json TEXT,
is_default BOOLEAN DEFAULT FALSE,
user_id BIGINT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE sys_dashboard_config IS '数据仪表盘配置';
COMMENT ON COLUMN sys_dashboard_config.dashboard_type IS '类型(HOME/DEPARTMENT/EXECUTIVE)';

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.basicmanage.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dashboard_config")
public class DashboardConfig extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("dashboard_name") private String dashboardName;
@TableField("dashboard_type") private String dashboardType;
@TableField("config_json") private String configJson;
@TableField("layout_json") private String layoutJson;
@TableField("is_default") private Boolean isDefault;
@TableField("user_id") private Long userId;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.basicmanage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.basicmanage.domain.DashboardConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DashboardConfigMapper extends BaseMapper<DashboardConfig> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.basicmanage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.basicmanage.domain.DashboardConfig;
public interface IDashboardConfigService extends IService<DashboardConfig> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.basicmanage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.basicmanage.domain.DashboardConfig;
import com.healthlink.his.basicmanage.mapper.DashboardConfigMapper;
import com.healthlink.his.basicmanage.service.IDashboardConfigService;
import org.springframework.stereotype.Service;
@Service
public class DashboardConfigServiceImpl extends ServiceImpl<DashboardConfigMapper, DashboardConfig> implements IDashboardConfigService {
}

View File

@@ -0,0 +1,23 @@
package com.healthlink.his.empi.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("empi_family_member")
public class EmpiFamilyMember extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("patient_id") private Long patientId;
@TableField("member_name") private String memberName;
@TableField("relationship") private String relationship;
@TableField("gender") private String gender;
@TableField("birth_date") private Date birthDate;
@TableField("phone") private String phone;
@TableField("id_card") private String idCard;
@TableField("is_emergency_contact") private Boolean isEmergencyContact;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.empi.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("empi_merge_log")
public class EmpiMergeLog extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("source_patient_id") private Long sourcePatientId;
@TableField("target_patient_id") private Long targetPatientId;
@TableField("merge_type") private String mergeType;
@TableField("merge_reason") private String mergeReason;
@TableField("merge_by") private String mergeBy;
@TableField("merge_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date mergeTime;
@TableField("undo_by") private String undoBy;
@TableField("undo_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date undoTime;
@TableField("undo_reason") private String undoReason;
@TableField("status") private String status;
}

View File

@@ -0,0 +1,21 @@
package com.healthlink.his.empi.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("empi_patient_photo")
public class EmpiPatientPhoto extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("patient_id") private Long patientId;
@TableField("photo_type") private String photoType;
@TableField("photo_data") private String photoData;
@TableField("upload_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date uploadTime;
@TableField("upload_by") private String uploadBy;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.empi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.empi.domain.EmpiFamilyMember;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmpiFamilyMemberMapper extends BaseMapper<EmpiFamilyMember> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.empi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.empi.domain.EmpiMergeLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmpiMergeLogMapper extends BaseMapper<EmpiMergeLog> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.empi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.empi.domain.EmpiPatientPhoto;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmpiPatientPhotoMapper extends BaseMapper<EmpiPatientPhoto> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.empi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.empi.domain.EmpiFamilyMember;
public interface IEmpiFamilyMemberService extends IService<EmpiFamilyMember> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.empi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.empi.domain.EmpiMergeLog;
public interface IEmpiMergeLogService extends IService<EmpiMergeLog> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.empi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.empi.domain.EmpiPatientPhoto;
public interface IEmpiPatientPhotoService extends IService<EmpiPatientPhoto> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.empi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.empi.domain.EmpiFamilyMember;
import com.healthlink.his.empi.mapper.EmpiFamilyMemberMapper;
import com.healthlink.his.empi.service.IEmpiFamilyMemberService;
import org.springframework.stereotype.Service;
@Service
public class EmpiFamilyMemberServiceImpl extends ServiceImpl<EmpiFamilyMemberMapper, EmpiFamilyMember> implements IEmpiFamilyMemberService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.empi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.empi.domain.EmpiMergeLog;
import com.healthlink.his.empi.mapper.EmpiMergeLogMapper;
import com.healthlink.his.empi.service.IEmpiMergeLogService;
import org.springframework.stereotype.Service;
@Service
public class EmpiMergeLogServiceImpl extends ServiceImpl<EmpiMergeLogMapper, EmpiMergeLog> implements IEmpiMergeLogService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.empi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.empi.domain.EmpiPatientPhoto;
import com.healthlink.his.empi.mapper.EmpiPatientPhotoMapper;
import com.healthlink.his.empi.service.IEmpiPatientPhotoService;
import org.springframework.stereotype.Service;
@Service
public class EmpiPatientPhotoServiceImpl extends ServiceImpl<EmpiPatientPhotoMapper, EmpiPatientPhoto> implements IEmpiPatientPhotoService {
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.quality.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("quality_core_indicator")
public class QualityCoreIndicator extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("indicator_code") private String indicatorCode;
@TableField("indicator_name") private String indicatorName;
@TableField("indicator_category") private String indicatorCategory;
@TableField("target_value") private BigDecimal targetValue;
@TableField("actual_value") private BigDecimal actualValue;
@TableField("unit") private String unit;
@TableField("stat_period") private String statPeriod;
@TableField("stat_date") private String statDate;
@TableField("department_id") private Long departmentId;
@TableField("department_name") private String departmentName;
@TableField("status") private String status;
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.quality.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("quality_order_statistics")
public class QualityOrderStatistics extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("stat_date") private String statDate;
@TableField("department_id") private Long departmentId;
@TableField("department_name") private String departmentName;
@TableField("total_orders") private Integer totalOrders;
@TableField("executed_orders") private Integer executedOrders;
@TableField("completed_orders") private Integer completedOrders;
@TableField("stopped_orders") private Integer stoppedOrders;
@TableField("cancelled_orders") private Integer cancelledOrders;
@TableField("execute_rate") private BigDecimal executeRate;
@TableField("complete_rate") private BigDecimal completeRate;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.quality.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.quality.domain.QualityCoreIndicator;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface QualityCoreIndicatorMapper extends BaseMapper<QualityCoreIndicator> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.quality.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.quality.domain.QualityOrderStatistics;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface QualityOrderStatisticsMapper extends BaseMapper<QualityOrderStatistics> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.quality.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.quality.domain.QualityCoreIndicator;
public interface IQualityCoreIndicatorService extends IService<QualityCoreIndicator> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.quality.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.quality.domain.QualityOrderStatistics;
public interface IQualityOrderStatisticsService extends IService<QualityOrderStatistics> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.quality.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.quality.domain.QualityCoreIndicator;
import com.healthlink.his.quality.mapper.QualityCoreIndicatorMapper;
import com.healthlink.his.quality.service.IQualityCoreIndicatorService;
import org.springframework.stereotype.Service;
@Service
public class QualityCoreIndicatorServiceImpl extends ServiceImpl<QualityCoreIndicatorMapper, QualityCoreIndicator> implements IQualityCoreIndicatorService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.quality.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.quality.domain.QualityOrderStatistics;
import com.healthlink.his.quality.mapper.QualityOrderStatisticsMapper;
import com.healthlink.his.quality.service.IQualityOrderStatisticsService;
import org.springframework.stereotype.Service;
@Service
public class QualityOrderStatisticsServiceImpl extends ServiceImpl<QualityOrderStatisticsMapper, QualityOrderStatistics> implements IQualityOrderStatisticsService {
}

View File

@@ -0,0 +1,3 @@
import request from '@/utils/request'
export function getDashboardOverview(){return request({url:'/dashboard/overview',method:'get'})}
export function getDashboardList(p){return request({url:'/dashboard/list',method:'get',params:p})}

View File

@@ -0,0 +1,30 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">数据仪表盘</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="text-align:center">
<h2 style="color:#409eff;margin:0">{{ overview.systemName||'HealthLink-HIS' }}</h2>
<p style="color:#666;margin:8px 0">版本: {{ overview.version }}</p>
</div>
</el-card>
<el-row :gutter="16">
<el-col :span="6" v-for="mod in overview.modules||[]" :key="mod">
<el-card shadow="hover" style="text-align:center;margin-bottom:16px">
<div style="font-size:20px;font-weight:bold;color:#409eff">{{ mod }}</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-top:16px">
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#409eff">{{ overview.totalTables||0 }}</div><div>数据库表</div></div></el-card></el-col>
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#67c23a">{{ overview.totalApis||0 }}</div><div>API接口</div></div></el-card></el-col>
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#e6a23c">{{ (overview.modules||[]).length }}</div><div>功能模块</div></div></el-card></el-col>
</el-row>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getDashboardOverview} from './api'
const overview=ref({})
onMounted(async()=>{const r=await getDashboardOverview();overview.value=r.data||{}})
</script>

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getPhotos(p){return request({url:'/empi-enhanced/photo/list',method:'get',params:p})}
export function addPhoto(d){return request({url:'/empi-enhanced/photo/add',method:'post',data:d})}
export function getFamilyMembers(p){return request({url:'/empi-enhanced/family/list',method:'get',params:p})}
export function addFamilyMember(d){return request({url:'/empi-enhanced/family/add',method:'post',data:d})}
export function deleteFamilyMember(id){return request({url:'/empi-enhanced/family/delete',method:'delete',params:{id}})}
export function getMergeLogPage(p){return request({url:'/empi-enhanced/merge-log/page',method:'get',params:p})}
export function addMergeLog(d){return request({url:'/empi-enhanced/merge-log/add',method:'post',data:d})}

View File

@@ -0,0 +1,54 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">EMPI增强</span></div>
<el-tabs v-model="tab" type="border-card">
<el-tab-pane label="家庭关系" name="family">
<div style="margin-bottom:12px">
<el-input v-model="searchPatientId" placeholder="患者ID" style="width:120px;margin-right:8px"/>
<el-button type="primary" @click="loadFamily">查询</el-button>
<el-button type="success" @click="showFamily=true">新增</el-button>
</div>
<el-table :data="familyData" border stripe>
<el-table-column prop="memberName" label="姓名" width="100"/>
<el-table-column prop="relationship" label="关系" width="80"/>
<el-table-column prop="gender" label="性别" width="60"/>
<el-table-column prop="phone" label="电话" width="130"/>
<el-table-column prop="isEmergencyContact" label="紧急联系人" width="100">
<template #default="{row}"><el-tag :type="row.isEmergencyContact?'success':'info'" size="small">{{ row.isEmergencyContact?'是':'否' }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{row}"><el-button type="danger" link size="small" @click="deleteFamily(row.id)">删除</el-button></template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="合并日志" name="mergeLog">
<el-table :data="mergeData" border stripe>
<el-table-column prop="sourcePatientId" label="源患者ID" width="110"/>
<el-table-column prop="targetPatientId" label="目标患者ID" width="110"/>
<el-table-column prop="mergeType" label="类型" width="80"/>
<el-table-column prop="mergeReason" label="原因" min-width="150" show-overflow-tooltip/>
<el-table-column prop="mergeBy" label="操作人" width="90"/>
<el-table-column prop="mergeTime" label="操作时间" width="170"/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}"><el-tag :type="row.status==='MERGED'?'success':'info'" size="small">{{ row.status==='MERGED'?'已合并':'已撤回' }}</el-tag></template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {getFamilyMembers,addFamilyMember,deleteFamilyMember,getMergeLogPage} from './api'
const tab=ref('family')
const searchPatientId=ref('')
const familyData=ref([]),mergeData=ref([])
const showFamily=ref(false)
const familyForm=reactive({patientId:null,memberName:'',relationship:'',gender:'',phone:'',isEmergencyContact:false})
const loadFamily=async()=>{if(!searchPatientId.value)return;const r=await getFamilyMembers({patientId:searchPatientId.value});familyData.value=r.data||[]}
const deleteFamily=async(id)=>{await ElMessageBox.confirm('确认删除?');await deleteFamilyMember(id);ElMessage.success('已删除');loadFamily()}
const loadData=async()=>{const m=await getMergeLogPage({pageNo:1,pageSize:50});mergeData.value=m.data?.records||[]}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,6 @@
import request from '@/utils/request'
export function getIndicatorPage(p){return request({url:'/quality-enhanced/indicator/page',method:'get',params:p})}
export function addIndicator(d){return request({url:'/quality-enhanced/indicator/add',method:'post',data:d})}
export function getIndicatorSummary(){return request({url:'/quality-enhanced/indicator/summary',method:'get'})}
export function getOrderStatsPage(p){return request({url:'/quality-enhanced/order-stats/page',method:'get',params:p})}
export function addOrderStats(d){return request({url:'/quality-enhanced/order-stats/add',method:'post',data:d})}

View File

@@ -0,0 +1,57 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">质量控制</span></div>
<el-tabs v-model="tab" type="border-card">
<el-tab-pane label="核心制度指标" name="indicator">
<el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:30px;text-align:center">
<div><div style="font-size:24px;font-weight:bold;color:#409eff">{{ summary.total||0 }}</div><div>总指标</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#67c23a">{{ summary.meetTarget||0 }}</div><div>达标</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#e6a23c">{{ summary.meetRate||0 }}%</div><div>达标率</div></div>
</div>
</el-card>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增指标</el-button></div>
<el-table :data="indicatorData" border stripe>
<el-table-column prop="indicatorCode" label="指标编码" width="120"/>
<el-table-column prop="indicatorName" label="指标名称" min-width="180"/>
<el-table-column prop="indicatorCategory" label="类别" width="100"/>
<el-table-column prop="targetValue" label="目标值" width="80" align="center"/>
<el-table-column prop="actualValue" label="实际值" width="80" align="center"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="statDate" label="统计日期" width="120"/>
</el-table>
</el-tab-pane>
<el-tab-pane label="医嘱统计" name="orderStats">
<div style="margin-bottom:12px"><el-button type="success" @click="showOrder=true">新增统计</el-button></div>
<el-table :data="orderData" border stripe>
<el-table-column prop="statDate" label="日期" width="120"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="totalOrders" label="总医嘱" width="80" align="center"/>
<el-table-column prop="executedOrders" label="已执行" width="80" align="center"/>
<el-table-column prop="completedOrders" label="已完成" width="80" align="center"/>
<el-table-column prop="executeRate" label="执行率%" width="80" align="center"/>
<el-table-column prop="completeRate" label="完成率%" width="80" align="center"/>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getIndicatorPage,addIndicator,getIndicatorSummary,getOrderStatsPage,addOrderStats} from './api'
const tab=ref('indicator')
const indicatorData=ref([]),orderData=ref([])
const summary=ref({})
const showAdd=ref(false),showOrder=ref(false)
const indicatorForm=reactive({indicatorCode:'',indicatorName:'',indicatorCategory:'',targetValue:null,actualValue:null,statDate:'',departmentName:''})
const orderForm=reactive({statDate:'',departmentName:'',totalOrders:0,executedOrders:0,completedOrders:0,stoppedOrders:0,cancelledOrders:0,executeRate:0,completeRate:0})
const loadData=async()=>{
const [i,s,o]=await Promise.all([getIndicatorPage({pageNo:1,pageSize:50}),getIndicatorSummary(),getOrderStatsPage({pageNo:1,pageSize:50})])
indicatorData.value=i.data?.records||[];summary.value=s.data||{};orderData.value=o.data?.records||[]
}
const submitIndicator=async()=>{await addIndicator(indicatorForm);ElMessage.success('新增成功');showAdd.value=false;loadData()}
const submitOrder=async()=>{await addOrderStats(orderForm);ElMessage.success('新增成功');showOrder.value=false;loadData()}
onMounted(()=>loadData())
</script>