feat(mobile): 移动护理APP医嘱执行+生命体征
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package com.healthlink.his.web.nursing.appservice;
|
||||
|
||||
import com.healthlink.his.web.nursing.dto.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface INursingMobileAppService {
|
||||
List<NursingMobilePatientDto> getMobilePatientList(String wardName, String searchKey);
|
||||
List<NursingMobileOrderDto> getMobileOrderList(Long patientId, Integer statusFilter);
|
||||
Map<String, Object> executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId);
|
||||
NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto vitalSign);
|
||||
NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.healthlink.his.web.nursing.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.nursing.domain.NursingVitalSignsChart;
|
||||
import com.healthlink.his.nursing.service.INursingVitalSignsChartService;
|
||||
import com.healthlink.his.web.nursing.appservice.INursingMobileAppService;
|
||||
import com.healthlink.his.web.nursing.dto.*;
|
||||
import com.healthlink.his.web.nursing.mapper.NursingMobileAppMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class NursingMobileAppServiceImpl implements INursingMobileAppService {
|
||||
|
||||
@Resource
|
||||
private NursingMobileAppMapper mobileMapper;
|
||||
|
||||
@Resource
|
||||
private INursingVitalSignsChartService vitalSignsChartService;
|
||||
|
||||
@Override
|
||||
public List<NursingMobilePatientDto> getMobilePatientList(String wardName, String searchKey) {
|
||||
return mobileMapper.selectMobilePatientList(wardName, searchKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NursingMobileOrderDto> getMobileOrderList(Long patientId, Integer statusFilter) {
|
||||
return mobileMapper.selectMobileOrderList(patientId, statusFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("requestId", requestId);
|
||||
result.put("adviceTable", adviceTable);
|
||||
result.put("executeTime", new Date());
|
||||
result.put("status", "SUCCESS");
|
||||
result.put("message", "医嘱执行成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto dto) {
|
||||
NursingVitalSignsChart chart = new NursingVitalSignsChart();
|
||||
chart.setEncounterId(dto.getEncounterId());
|
||||
chart.setPatientId(dto.getPatientId());
|
||||
chart.setPatientName(dto.getPatientName());
|
||||
chart.setRecordDate(dto.getRecordDate() != null ?
|
||||
new java.sql.Date(dto.getRecordDate().getTime()).toLocalDate() : LocalDate.now());
|
||||
chart.setRecordHour(dto.getRecordHour() != null ? dto.getRecordHour() : Calendar.getInstance().get(Calendar.HOUR_OF_DAY));
|
||||
chart.setTemperature(dto.getTemperature());
|
||||
chart.setPulse(dto.getPulse());
|
||||
chart.setRespiration(dto.getRespiration());
|
||||
chart.setSystolicBp(dto.getSystolicBp());
|
||||
chart.setDiastolicBp(dto.getDiastolicBp());
|
||||
chart.setHeightCm(dto.getHeightCm());
|
||||
chart.setWeightKg(dto.getWeightKg());
|
||||
chart.setPainScore(dto.getPainScore());
|
||||
chart.setConsciousLevel(dto.getConsciousLevel());
|
||||
chart.setInputMl(dto.getInputMl());
|
||||
chart.setOutputMl(dto.getOutputMl());
|
||||
chart.setStoolCount(dto.getStoolCount());
|
||||
chart.setNurseName(dto.getNurseName());
|
||||
chart.setCreateTime(new Date());
|
||||
vitalSignsChartService.save(chart);
|
||||
dto.setId(chart.getId());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days) {
|
||||
NursingMobileVitalSignTrendDto trend = new NursingMobileVitalSignTrendDto();
|
||||
trend.setPatientId(patientId);
|
||||
|
||||
LocalDate endDate = LocalDate.now();
|
||||
LocalDate startDate = endDate.minusDays(days != null ? days : 7);
|
||||
|
||||
LambdaQueryWrapper<NursingVitalSignsChart> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(NursingVitalSignsChart::getPatientId, patientId)
|
||||
.ge(NursingVitalSignsChart::getRecordDate, startDate)
|
||||
.le(NursingVitalSignsChart::getRecordDate, endDate)
|
||||
.orderByAsc(NursingVitalSignsChart::getRecordDate)
|
||||
.orderByAsc(NursingVitalSignsChart::getRecordHour);
|
||||
|
||||
List<NursingVitalSignsChart> records = vitalSignsChartService.list(wrapper);
|
||||
|
||||
List<NursingMobileVitalSignTrendDto.VitalSignPoint> tempPoints = new ArrayList<>();
|
||||
List<NursingMobileVitalSignTrendDto.VitalSignPoint> pulsePoints = new ArrayList<>();
|
||||
List<NursingMobileVitalSignTrendDto.VitalSignPoint> sysPoints = new ArrayList<>();
|
||||
List<NursingMobileVitalSignTrendDto.VitalSignPoint> diaPoints = new ArrayList<>();
|
||||
List<NursingMobileVitalSignTrendDto.VitalSignPoint> respPoints = new ArrayList<>();
|
||||
|
||||
for (NursingVitalSignsChart r : records) {
|
||||
String label = r.getRecordDate() + " " + (r.getRecordHour() != null ? r.getRecordHour() + ":00" : "");
|
||||
Date dateVal = java.sql.Date.valueOf(r.getRecordDate());
|
||||
|
||||
if (r.getTemperature() != null) {
|
||||
NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint();
|
||||
p.setRecordDate(dateVal);
|
||||
p.setRecordHour(r.getRecordHour());
|
||||
p.setValue(r.getTemperature());
|
||||
p.setLabel(label);
|
||||
tempPoints.add(p);
|
||||
}
|
||||
if (r.getPulse() != null) {
|
||||
NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint();
|
||||
p.setRecordDate(dateVal);
|
||||
p.setRecordHour(r.getRecordHour());
|
||||
p.setValue(BigDecimal.valueOf(r.getPulse()));
|
||||
p.setLabel(label);
|
||||
pulsePoints.add(p);
|
||||
}
|
||||
if (r.getSystolicBp() != null) {
|
||||
NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint();
|
||||
p.setRecordDate(dateVal);
|
||||
p.setRecordHour(r.getRecordHour());
|
||||
p.setValue(BigDecimal.valueOf(r.getSystolicBp()));
|
||||
p.setLabel(label);
|
||||
sysPoints.add(p);
|
||||
}
|
||||
if (r.getDiastolicBp() != null) {
|
||||
NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint();
|
||||
p.setRecordDate(dateVal);
|
||||
p.setRecordHour(r.getRecordHour());
|
||||
p.setValue(BigDecimal.valueOf(r.getDiastolicBp()));
|
||||
p.setLabel(label);
|
||||
diaPoints.add(p);
|
||||
}
|
||||
if (r.getRespiration() != null) {
|
||||
NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint();
|
||||
p.setRecordDate(dateVal);
|
||||
p.setRecordHour(r.getRecordHour());
|
||||
p.setValue(BigDecimal.valueOf(r.getRespiration()));
|
||||
p.setLabel(label);
|
||||
respPoints.add(p);
|
||||
}
|
||||
|
||||
if (!records.isEmpty()) {
|
||||
trend.setPatientName(records.get(0).getPatientName());
|
||||
}
|
||||
}
|
||||
|
||||
trend.setTemperatureData(tempPoints);
|
||||
trend.setPulseData(pulsePoints);
|
||||
trend.setSystolicBpData(sysPoints);
|
||||
trend.setDiastolicBpData(diaPoints);
|
||||
trend.setRespirationData(respPoints);
|
||||
|
||||
return trend;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.healthlink.his.web.nursing.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.nursing.appservice.INursingMobileAppService;
|
||||
import com.healthlink.his.web.nursing.dto.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "移动护理")
|
||||
@RestController
|
||||
@RequestMapping("/nursing/mobile")
|
||||
public class NursingMobileController {
|
||||
|
||||
@Resource
|
||||
private INursingMobileAppService mobileAppService;
|
||||
|
||||
@Operation(summary = "移动端患者列表")
|
||||
@GetMapping("/patient-list")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:list')")
|
||||
public R<?> getPatientList(
|
||||
@RequestParam(required = false) String wardName,
|
||||
@RequestParam(required = false) String searchKey) {
|
||||
List<NursingMobilePatientDto> list = mobileAppService.getMobilePatientList(wardName, searchKey);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "待执行医嘱列表")
|
||||
@GetMapping("/order-list/{patientId}")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:list')")
|
||||
public R<?> getOrderList(
|
||||
@PathVariable Long patientId,
|
||||
@RequestParam(required = false) Integer statusFilter) {
|
||||
List<NursingMobileOrderDto> list = mobileAppService.getMobileOrderList(patientId, statusFilter);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "扫码执行医嘱")
|
||||
@PostMapping("/order-execute")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
|
||||
public R<?> executeOrder(@RequestBody Map<String, Object> params) {
|
||||
Long requestId = Long.valueOf(params.get("requestId").toString());
|
||||
String adviceTable = params.get("adviceTable").toString();
|
||||
Long encounterId = Long.valueOf(params.get("encounterId").toString());
|
||||
Long patientId = Long.valueOf(params.get("patientId").toString());
|
||||
Map<String, Object> result = mobileAppService.executeOrder(requestId, adviceTable, encounterId, patientId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "录入生命体征")
|
||||
@PostMapping("/vital-sign")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
|
||||
public R<?> saveVitalSign(@RequestBody NursingMobileVitalSignDto vitalSign) {
|
||||
NursingMobileVitalSignDto saved = mobileAppService.saveVitalSign(vitalSign);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@Operation(summary = "体征趋势")
|
||||
@GetMapping("/vital-sign-trend/{patientId}")
|
||||
@PreAuthorize("hasAuthority('nursing:nursing:list')")
|
||||
public R<?> getVitalSignTrend(
|
||||
@PathVariable Long patientId,
|
||||
@RequestParam(required = false) Integer days) {
|
||||
NursingMobileVitalSignTrendDto trend = mobileAppService.getVitalSignTrend(patientId, days);
|
||||
return R.ok(trend);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class NursingMobileOrderDto {
|
||||
private Long requestId;
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String adviceName;
|
||||
private String adviceTable;
|
||||
private Integer requestStatus;
|
||||
private String requestStatusText;
|
||||
private Integer therapyEnum;
|
||||
private String therapyEnumText;
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private String requesterName;
|
||||
private String frequencyUsage;
|
||||
private String singleDose;
|
||||
private String volume;
|
||||
private Integer quantity;
|
||||
private String unitCodeText;
|
||||
private Integer executeCount;
|
||||
private Integer executeNum;
|
||||
private Date lastExecuteTime;
|
||||
private String barcode;
|
||||
private Long procedureId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class NursingMobilePatientDto {
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private Integer genderEnum;
|
||||
private String genderEnumText;
|
||||
private String bedName;
|
||||
private String wardName;
|
||||
private Integer nursingLevel;
|
||||
private String nursingLevelText;
|
||||
private Integer encounterStatus;
|
||||
private String encounterStatusText;
|
||||
private String diagnosis;
|
||||
private String admittingDoctorName;
|
||||
private Date admissionDate;
|
||||
private Integer priorityEnum;
|
||||
private String priorityEnumText;
|
||||
private Integer age;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class NursingMobileVitalSignDto {
|
||||
private Long id;
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private Date recordDate;
|
||||
private Integer recordHour;
|
||||
private BigDecimal temperature;
|
||||
private Integer pulse;
|
||||
private Integer respiration;
|
||||
private Integer systolicBp;
|
||||
private Integer diastolicBp;
|
||||
private BigDecimal heightCm;
|
||||
private BigDecimal weightKg;
|
||||
private Integer painScore;
|
||||
private String consciousLevel;
|
||||
private Integer inputMl;
|
||||
private Integer outputMl;
|
||||
private Integer stoolCount;
|
||||
private String nurseName;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.healthlink.his.web.nursing.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class NursingMobileVitalSignTrendDto {
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private List<VitalSignPoint> temperatureData;
|
||||
private List<VitalSignPoint> pulseData;
|
||||
private List<VitalSignPoint> systolicBpData;
|
||||
private List<VitalSignPoint> diastolicBpData;
|
||||
private List<VitalSignPoint> respirationData;
|
||||
|
||||
@Data
|
||||
public static class VitalSignPoint {
|
||||
private Date recordDate;
|
||||
private Integer recordHour;
|
||||
private BigDecimal value;
|
||||
private String label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.healthlink.his.web.nursing.mapper;
|
||||
|
||||
import com.healthlink.his.web.nursing.dto.NursingMobileOrderDto;
|
||||
import com.healthlink.his.web.nursing.dto.NursingMobilePatientDto;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface NursingMobileAppMapper {
|
||||
List<NursingMobilePatientDto> selectMobilePatientList(
|
||||
@Param("wardName") String wardName,
|
||||
@Param("searchKey") String searchKey);
|
||||
|
||||
List<NursingMobileOrderDto> selectMobileOrderList(
|
||||
@Param("patientId") Long patientId,
|
||||
@Param("statusFilter") Integer statusFilter);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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.web.nursing.mapper.NursingMobileAppMapper">
|
||||
|
||||
<select id="selectMobilePatientList"
|
||||
resultType="com.healthlink.his.web.nursing.dto.NursingMobilePatientDto">
|
||||
SELECT ae.id AS encounterId,
|
||||
ap.id AS patientId,
|
||||
ap.name AS patientName,
|
||||
ap.gender_enum AS genderEnum,
|
||||
CASE ap.gender_enum WHEN 1 THEN '男' WHEN 2 THEN '女' ELSE '未知' END AS genderEnumText,
|
||||
alb.location_name AS bedName,
|
||||
alw.location_name AS wardName,
|
||||
ae.priority_enum AS nursingLevel,
|
||||
CASE ae.priority_enum WHEN 1 THEN '一级' WHEN 2 THEN '二级' WHEN 3 THEN '三级' WHEN 4 THEN '特级' ELSE '普通' END AS nursingLevelText,
|
||||
ae.status_enum AS encounterStatus,
|
||||
ae.admitting_doctor_name AS admittingDoctorName,
|
||||
ae.priority_enum AS priorityEnum,
|
||||
CASE ae.priority_enum WHEN 1 THEN '急' WHEN 2 THEN '危' WHEN 3 THEN '一般' ELSE '普通' END AS priorityEnumText
|
||||
FROM adm_encounter ae
|
||||
LEFT JOIN adm_patient ap ON ae.patient_id = ap.id AND ap.delete_flag = '0'
|
||||
LEFT JOIN (SELECT ael.encounter_id, al.name AS location_name
|
||||
FROM adm_encounter_location ael
|
||||
LEFT JOIN adm_location al ON ael.location_id = al.id AND al.delete_flag = '0'
|
||||
WHERE ael.status_enum = 2 AND ael.delete_flag = '0'
|
||||
AND al.form_enum = 4) alw ON alw.encounter_id = ae.id
|
||||
LEFT JOIN (SELECT ael.encounter_id, al.name AS location_name
|
||||
FROM adm_encounter_location ael
|
||||
LEFT JOIN adm_location al ON ael.location_id = al.id AND al.delete_flag = '0'
|
||||
WHERE ael.status_enum = 2 AND ael.delete_flag = '0'
|
||||
AND al.form_enum = 6) alb ON alb.encounter_id = ae.id
|
||||
WHERE ae.delete_flag = '0'
|
||||
AND ae.class_enum = 3
|
||||
AND ae.status_enum IN (2, 3, 6)
|
||||
AND (#{wardName} IS NULL OR alw.location_name = #{wardName})
|
||||
AND (#{searchKey} IS NULL OR ap.name LIKE '%' || #{searchKey} || '%'
|
||||
OR ap.py_str LIKE '%' || #{searchKey} || '%'
|
||||
OR alb.location_name LIKE '%' || #{searchKey} || '%')
|
||||
ORDER BY ae.priority_enum ASC, alb.location_name ASC
|
||||
</select>
|
||||
|
||||
<select id="selectMobileOrderList"
|
||||
resultType="com.healthlink.his.web.nursing.dto.NursingMobileOrderDto">
|
||||
SELECT
|
||||
COALESCE(mr.id, sr.id, dr.id) AS requestId,
|
||||
COALESCE(mr.encounter_id, sr.encounter_id, dr.encounter_id) AS encounterId,
|
||||
COALESCE(mr.patient_id, sr.patient_id, dr.patient_id) AS patientId,
|
||||
COALESCE(mr.advice_name, sr.advice_name, dr.advice_name) AS adviceName,
|
||||
CASE
|
||||
WHEN mr.id IS NOT NULL THEN 'med_medication_request'
|
||||
WHEN sr.id IS NOT NULL THEN 'wor_service_request'
|
||||
WHEN dr.id IS NOT NULL THEN 'wor_device_request'
|
||||
END AS adviceTable,
|
||||
COALESCE(mr.status_enum, sr.status_enum, dr.status_enum) AS requestStatus,
|
||||
CASE COALESCE(mr.status_enum, sr.status_enum, dr.status_enum)
|
||||
WHEN 2 THEN '执行中'
|
||||
WHEN 3 THEN '已完成'
|
||||
WHEN 6 THEN '已停止'
|
||||
WHEN 10 THEN '已校对'
|
||||
WHEN 11 THEN '待接收'
|
||||
ELSE '未知'
|
||||
END AS requestStatusText,
|
||||
COALESCE(mr.therapy_enum, sr.therapy_enum, dr.therapy_enum) AS therapyEnum,
|
||||
CASE COALESCE(mr.therapy_enum, sr.therapy_enum, dr.therapy_enum)
|
||||
WHEN 1 THEN '长期'
|
||||
WHEN 2 THEN '临时'
|
||||
ELSE '未知'
|
||||
END AS therapyEnumText,
|
||||
COALESCE(mr.start_time, sr.start_time, dr.start_time) AS startTime,
|
||||
COALESCE(mr.end_time, sr.end_time, dr.end_time) AS endTime,
|
||||
COALESCE(mr.requester_name, sr.requester_name, dr.requester_name) AS requesterName,
|
||||
COALESCE(mr.rate_code, sr.rate_code, '') AS frequencyUsage,
|
||||
COALESCE(mr.dose, 0) AS singleDose,
|
||||
COALESCE(mr.volume, sr.volume, '') AS volume,
|
||||
COALESCE(mr.quantity, sr.quantity, dr.quantity, 0) AS quantity,
|
||||
COALESCE(mr.unit_code, sr.unit_code, dr.unit_code, '') AS unitCodeText
|
||||
FROM (
|
||||
SELECT id, encounter_id, patient_id, advice_name, status_enum, therapy_enum,
|
||||
start_time, end_time, requester_name, rate_code, dose, volume, quantity, unit_code
|
||||
FROM med_medication_request
|
||||
WHERE delete_flag = '0' AND status_enum IN (2, 3, 10, 11)
|
||||
AND patient_id = #{patientId}
|
||||
) mr
|
||||
LEFT JOIN (
|
||||
SELECT id, encounter_id, patient_id, advice_name, status_enum, therapy_enum,
|
||||
start_time, end_time, requester_name, rate_code, volume, quantity, unit_code
|
||||
FROM wor_service_request
|
||||
WHERE delete_flag = '0' AND status_enum IN (2, 3, 10, 11)
|
||||
AND patient_id = #{patientId}
|
||||
) sr ON 1=0
|
||||
LEFT JOIN (
|
||||
SELECT id, encounter_id, patient_id, advice_name, status_enum, therapy_enum,
|
||||
start_time, end_time, requester_name, volume, quantity, unit_code
|
||||
FROM wor_device_request
|
||||
WHERE delete_flag = '0' AND status_enum IN (2, 3, 10, 11)
|
||||
AND patient_id = #{patientId}
|
||||
) dr ON 1=0
|
||||
ORDER BY
|
||||
CASE COALESCE(mr.status_enum, sr.status_enum, dr.status_enum) WHEN 2 THEN 0 WHEN 10 THEN 1 ELSE 2 END,
|
||||
COALESCE(mr.therapy_enum, sr.therapy_enum, dr.therapy_enum) ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -129,6 +129,37 @@ export const constantRoutes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/nursingmobile',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'patient-list',
|
||||
component: () => import('@/views/nursingmobile/PatientList.vue'),
|
||||
name: 'NursingMobilePatientList',
|
||||
meta: {title: '移动护理-患者列表'}
|
||||
},
|
||||
{
|
||||
path: 'order-list',
|
||||
component: () => import('@/views/nursingmobile/OrderList.vue'),
|
||||
name: 'NursingMobileOrderList',
|
||||
meta: {title: '移动护理-医嘱列表'}
|
||||
},
|
||||
{
|
||||
path: 'vital-sign',
|
||||
component: () => import('@/views/nursingmobile/VitalSign.vue'),
|
||||
name: 'NursingMobileVitalSign',
|
||||
meta: {title: '移动护理-生命体征录入'}
|
||||
},
|
||||
{
|
||||
path: 'vital-sign-trend',
|
||||
component: () => import('@/views/nursingmobile/VitalSignTrend.vue'),
|
||||
name: 'NursingMobileVitalSignTrend',
|
||||
meta: {title: '移动护理-体征趋势'}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 添加套餐管理相关路由到公共路由,确保始终可用
|
||||
{
|
||||
path: '/maintainSystem/Inspection/PackageManagement',
|
||||
|
||||
245
healthlink-his-ui/src/views/nursingmobile/OrderList.vue
Normal file
245
healthlink-his-ui/src/views/nursingmobile/OrderList.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="mobile-order-list">
|
||||
<div class="page-header">
|
||||
<el-page-header @back="goBack">
|
||||
<template #content>
|
||||
<span class="page-title">{{ patientName }} ({{ bedName }})</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-radio-group v-model="statusFilter" size="small" @change="fetchOrders">
|
||||
<el-radio-button :value="2">执行中</el-radio-button>
|
||||
<el-radio-button :value="10">已校对</el-radio-button>
|
||||
<el-radio-button :value="null">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" size="small" @click="handleScan">
|
||||
扫码执行
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="order-list">
|
||||
<div
|
||||
v-for="order in orderList"
|
||||
:key="order.requestId"
|
||||
class="order-item"
|
||||
>
|
||||
<div class="order-header">
|
||||
<span class="order-name">{{ order.adviceName }}</span>
|
||||
<el-tag :type="getStatusType(order.requestStatus)" size="small">
|
||||
{{ order.requestStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="order-body">
|
||||
<div class="info-row">
|
||||
<span class="label">类型:</span>
|
||||
<el-tag size="small">{{ order.therapyEnumText }}</el-tag>
|
||||
</div>
|
||||
<div v-if="order.frequencyUsage" class="info-row">
|
||||
<span class="label">频次:</span>
|
||||
<span class="value">{{ order.frequencyUsage }}</span>
|
||||
</div>
|
||||
<div v-if="order.singleDose" class="info-row">
|
||||
<span class="label">剂量:</span>
|
||||
<span class="value">{{ order.singleDose }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">开嘱医生:</span>
|
||||
<span class="value">{{ order.requesterName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-footer">
|
||||
<el-button
|
||||
v-if="order.requestStatus === 2 || order.requestStatus === 10"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleExecute(order)"
|
||||
>
|
||||
执行
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && orderList.length === 0" description="暂无医嘱" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="scanDialogVisible" title="扫码执行" width="400px">
|
||||
<el-form :model="scanForm" label-width="80px">
|
||||
<el-form-item label="条码">
|
||||
<el-input v-model="scanForm.barcode" placeholder="请扫描或输入条码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="scanDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmScan">确认执行</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getMobileOrderList, executeOrder } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const statusFilter = ref(2)
|
||||
const orderList = ref([])
|
||||
const scanDialogVisible = ref(false)
|
||||
const scanForm = ref({ barcode: '' })
|
||||
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const bedName = ref(route.query.bedName || '')
|
||||
const patientId = ref(route.query.patientId)
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!patientId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMobileOrderList(patientId.value, { statusFilter: statusFilter.value })
|
||||
orderList.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/nursingmobile/patient-list')
|
||||
}
|
||||
|
||||
const handleScan = () => {
|
||||
scanForm.value.barcode = ''
|
||||
scanDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmScan = () => {
|
||||
if (!scanForm.value.barcode) {
|
||||
ElMessage.warning('请输入条码')
|
||||
return
|
||||
}
|
||||
const matchedOrder = orderList.value.find(o => o.barcode === scanForm.value.barcode)
|
||||
if (matchedOrder) {
|
||||
handleExecute(matchedOrder)
|
||||
} else {
|
||||
ElMessage.error('未找到匹配的医嘱')
|
||||
}
|
||||
scanDialogVisible.value = false
|
||||
}
|
||||
|
||||
const handleExecute = async (order) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认执行医嘱: ${order.adviceName}?`,
|
||||
'确认执行',
|
||||
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
loading.value = true
|
||||
await executeOrder({
|
||||
requestId: order.requestId,
|
||||
adviceTable: order.adviceTable,
|
||||
encounterId: order.encounterId,
|
||||
patientId: order.patientId
|
||||
})
|
||||
ElMessage.success('执行成功')
|
||||
fetchOrders()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error('执行失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const map = { 2: 'primary', 3: 'success', 6: 'info', 10: 'warning', 11: '' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-order-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.order-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.order-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
201
healthlink-his-ui/src/views/nursingmobile/PatientList.vue
Normal file
201
healthlink-his-ui/src/views/nursingmobile/PatientList.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="mobile-patient-list">
|
||||
<div class="page-header">
|
||||
<h2>患者列表</h2>
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
placeholder="搜索姓名/床号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="patient-cards">
|
||||
<div
|
||||
v-for="patient in patientList"
|
||||
:key="patient.encounterId"
|
||||
class="patient-card"
|
||||
@click="handlePatientClick(patient)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="patient-name">{{ patient.patientName }}</span>
|
||||
<el-tag :type="getGenderType(patient.genderEnum)" size="small">
|
||||
{{ patient.genderEnumText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="label">床号:</span>
|
||||
<span class="value">{{ patient.bedName || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">护理等级:</span>
|
||||
<el-tag :type="getNursingLevelType(patient.nursingLevel)" size="small">
|
||||
{{ patient.nursingLevelText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">病情:</span>
|
||||
<el-tag :type="getPriorityType(patient.priorityEnum)" size="small">
|
||||
{{ patient.priorityEnumText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="patient.diagnosis" class="info-row">
|
||||
<span class="label">诊断:</span>
|
||||
<span class="value diagnosis">{{ patient.diagnosis }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="doctor">{{ patient.admittingDoctorName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && patientList.length === 0" description="暂无患者" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getMobilePatientList } from './api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const searchKey = ref('')
|
||||
const patientList = ref([])
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMobilePatientList({ searchKey: searchKey.value })
|
||||
patientList.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePatientClick = (patient) => {
|
||||
router.push({
|
||||
path: '/nursingmobile/order-list',
|
||||
query: {
|
||||
encounterId: patient.encounterId,
|
||||
patientId: patient.patientId,
|
||||
patientName: patient.patientName,
|
||||
bedName: patient.bedName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getGenderType = (gender) => {
|
||||
return gender === 1 ? 'primary' : gender === 2 ? 'danger' : 'info'
|
||||
}
|
||||
|
||||
const getNursingLevelType = (level) => {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: '', 4: 'danger' }
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
const getPriorityType = (priority) => {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: '', 4: 'info' }
|
||||
return map[priority] || 'info'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-patient-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.patient-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.patient-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.patient-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-row .diagnosis {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.doctor {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
195
healthlink-his-ui/src/views/nursingmobile/VitalSign.vue
Normal file
195
healthlink-his-ui/src/views/nursingmobile/VitalSign.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="mobile-vital-sign">
|
||||
<div class="page-header">
|
||||
<el-page-header @back="goBack">
|
||||
<template #content>
|
||||
<span class="page-title">生命体征录入</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
class="vital-form"
|
||||
>
|
||||
<el-form-item label="患者">
|
||||
<span class="patient-info">{{ patientName }} ({{ bedName }})</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="记录时间">
|
||||
<el-date-picker
|
||||
v-model="form.recordDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="时点">
|
||||
<el-select v-model="form.recordHour" placeholder="选择时点" style="width: 100%">
|
||||
<el-option v-for="h in 24" :key="h-1" :label="(h-1)+':00'" :value="h-1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="体温" prop="temperature">
|
||||
<el-input-number v-model="form.temperature" :min="35" :max="42" :step="0.1" :precision="1" style="width: 100%" />
|
||||
<span class="unit">°C</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="脉搏" prop="pulse">
|
||||
<el-input-number v-model="form.pulse" :min="40" :max="200" style="width: 100%" />
|
||||
<span class="unit">次/分</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="呼吸" prop="respiration">
|
||||
<el-input-number v-model="form.respiration" :min="10" :max="60" style="width: 100%" />
|
||||
<span class="unit">次/分</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收缩压" prop="systolicBp">
|
||||
<el-input-number v-model="form.systolicBp" :min="60" :max="300" style="width: 100%" />
|
||||
<span class="unit">mmHg</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="舒张压" prop="diastolicBp">
|
||||
<el-input-number v-model="form.diastolicBp" :min="30" :max="200" style="width: 100%" />
|
||||
<span class="unit">mmHg</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="疼痛评分">
|
||||
<el-rate v-model="form.painScore" :max="10" show-score />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="意识">
|
||||
<el-select v-model="form.consciousLevel" placeholder="选择意识状态" style="width: 100%">
|
||||
<el-option label="清醒" value="清醒" />
|
||||
<el-option label="嗜睡" value="嗜睡" />
|
||||
<el-option label="模糊" value="模糊" />
|
||||
<el-option label="昏睡" value="昏睡" />
|
||||
<el-option label="浅昏迷" value="浅昏迷" />
|
||||
<el-option label="深昏迷" value="深昏迷" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="入量">
|
||||
<el-input-number v-model="form.inputMl" :min="0" style="width: 100%" />
|
||||
<span class="unit">ml</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="出量">
|
||||
<el-input-number v-model="form.outputMl" :min="0" style="width: 100%" />
|
||||
<span class="unit">ml</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" style="width: 100%" @click="handleSubmit">
|
||||
保存
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { saveVitalSign } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const bedName = ref(route.query.bedName || '')
|
||||
const patientId = ref(route.query.patientId)
|
||||
const encounterId = ref(route.query.encounterId)
|
||||
|
||||
const now = new Date()
|
||||
const form = reactive({
|
||||
patientId: patientId.value ? Number(patientId.value) : null,
|
||||
encounterId: encounterId.value ? Number(encounterId.value) : null,
|
||||
patientName: patientName.value,
|
||||
recordDate: now.toISOString().split('T')[0],
|
||||
recordHour: now.getHours(),
|
||||
temperature: null,
|
||||
pulse: null,
|
||||
respiration: null,
|
||||
systolicBp: null,
|
||||
diastolicBp: null,
|
||||
painScore: 0,
|
||||
consciousLevel: '清醒',
|
||||
inputMl: null,
|
||||
outputMl: null,
|
||||
nurseName: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
temperature: [{ required: true, message: '请输入体温', trigger: 'blur' }],
|
||||
pulse: [{ required: true, message: '请输入脉搏', trigger: 'blur' }],
|
||||
systolicBp: [{ required: true, message: '请输入收缩压', trigger: 'blur' }],
|
||||
diastolicBp: [{ required: true, message: '请输入舒张压', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/nursingmobile/patient-list')
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
await saveVitalSign(form)
|
||||
ElMessage.success('保存成功')
|
||||
router.push('/nursingmobile/vital-sign-trend?patientId=' + patientId.value + '&patientName=' + patientName.value)
|
||||
} catch (e) {
|
||||
if (e !== false) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-vital-sign {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vital-form {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.unit {
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
251
healthlink-his-ui/src/views/nursingmobile/VitalSignTrend.vue
Normal file
251
healthlink-his-ui/src/views/nursingmobile/VitalSignTrend.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="mobile-vital-trend">
|
||||
<div class="page-header">
|
||||
<el-page-header @back="goBack">
|
||||
<template #content>
|
||||
<span class="page-title">体征趋势</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
|
||||
<div class="patient-info">
|
||||
<span>{{ patientName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="days-filter">
|
||||
<el-radio-group v-model="days" size="small" @change="fetchTrend">
|
||||
<el-radio-button :value="3">3天</el-radio-button>
|
||||
<el-radio-button :value="7">7天</el-radio-button>
|
||||
<el-radio-button :value="14">14天</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="trend-charts">
|
||||
<div class="chart-section">
|
||||
<h4>体温 (°C)</h4>
|
||||
<div class="chart-container">
|
||||
<div v-if="trendData.temperatureData.length === 0" class="no-data">暂无数据</div>
|
||||
<div v-else class="simple-chart">
|
||||
<div v-for="(point, idx) in trendData.temperatureData" :key="idx" class="chart-point">
|
||||
<div class="point-value">{{ point.value }}</div>
|
||||
<div class="point-bar" :style="{ height: getBarHeight(point.value, 35, 42) + 'px' }" />
|
||||
<div class="point-label">{{ formatLabel(point.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h4>脉搏 (次/分)</h4>
|
||||
<div class="chart-container">
|
||||
<div v-if="trendData.pulseData.length === 0" class="no-data">暂无数据</div>
|
||||
<div v-else class="simple-chart">
|
||||
<div v-for="(point, idx) in trendData.pulseData" :key="idx" class="chart-point">
|
||||
<div class="point-value">{{ point.value }}</div>
|
||||
<div class="point-bar pulse" :style="{ height: getBarHeight(point.value, 40, 120) + 'px' }" />
|
||||
<div class="point-label">{{ formatLabel(point.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h4>血压 (mmHg)</h4>
|
||||
<div class="chart-container">
|
||||
<div v-if="trendData.systolicBpData.length === 0" class="no-data">暂无数据</div>
|
||||
<div v-else class="simple-chart">
|
||||
<div v-for="(point, idx) in trendData.systolicBpData" :key="idx" class="chart-point">
|
||||
<div class="point-value">{{ point.value }}/{{ getDiastolicValue(idx) }}</div>
|
||||
<div class="point-bar bp" :style="{ height: getBarHeight(point.value, 60, 200) + 'px' }" />
|
||||
<div class="point-label">{{ formatLabel(point.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h4>呼吸 (次/分)</h4>
|
||||
<div class="chart-container">
|
||||
<div v-if="trendData.respirationData.length === 0" class="no-data">暂无数据</div>
|
||||
<div v-else class="simple-chart">
|
||||
<div v-for="(point, idx) in trendData.respirationData" :key="idx" class="chart-point">
|
||||
<div class="point-value">{{ point.value }}</div>
|
||||
<div class="point-bar resp" :style="{ height: getBarHeight(point.value, 10, 40) + 'px' }" />
|
||||
<div class="point-label">{{ formatLabel(point.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getVitalSignTrend } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const days = ref(7)
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const patientId = ref(route.query.patientId)
|
||||
|
||||
const trendData = ref({
|
||||
temperatureData: [],
|
||||
pulseData: [],
|
||||
systolicBpData: [],
|
||||
diastolicBpData: [],
|
||||
respirationData: []
|
||||
})
|
||||
|
||||
const fetchTrend = async () => {
|
||||
if (!patientId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getVitalSignTrend(patientId.value, { days: days.value })
|
||||
trendData.value = res.data || {
|
||||
temperatureData: [],
|
||||
pulseData: [],
|
||||
systolicBpData: [],
|
||||
diastolicBpData: [],
|
||||
respirationData: []
|
||||
}
|
||||
if (res.data?.patientName) {
|
||||
patientName.value = res.data.patientName
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/nursingmobile/patient-list')
|
||||
}
|
||||
|
||||
const getBarHeight = (value, min, max) => {
|
||||
if (!value) return 0
|
||||
const normalized = (value - min) / (max - min)
|
||||
return Math.max(10, Math.min(80, normalized * 80))
|
||||
}
|
||||
|
||||
const getDiastolicValue = (idx) => {
|
||||
const point = trendData.value.diastolicBpData[idx]
|
||||
return point ? point.value : '-'
|
||||
}
|
||||
|
||||
const formatLabel = (label) => {
|
||||
if (!label) return ''
|
||||
const parts = label.split(' ')
|
||||
return parts.length > 1 ? parts[1] : label
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTrend()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-vital-trend {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.days-filter {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.trend-charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chart-section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.simple-chart {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.chart-point {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.point-value {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.point-bar {
|
||||
width: 20px;
|
||||
background: #409eff;
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
.point-bar.pulse {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
.point-bar.bp {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
.point-bar.resp {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
.point-label {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
writing-mode: vertical-rl;
|
||||
max-height: 60px;
|
||||
}
|
||||
</style>
|
||||
21
healthlink-his-ui/src/views/nursingmobile/api.js
Normal file
21
healthlink-his-ui/src/views/nursingmobile/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getMobilePatientList(params) {
|
||||
return request({ url: '/nursing/mobile/patient-list', method: 'get', params })
|
||||
}
|
||||
|
||||
export function getMobileOrderList(patientId, params) {
|
||||
return request({ url: '/nursing/mobile/order-list/' + patientId, method: 'get', params })
|
||||
}
|
||||
|
||||
export function executeOrder(data) {
|
||||
return request({ url: '/nursing/mobile/order-execute', method: 'post', data })
|
||||
}
|
||||
|
||||
export function saveVitalSign(data) {
|
||||
return request({ url: '/nursing/mobile/vital-sign', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getVitalSignTrend(patientId, params) {
|
||||
return request({ url: '/nursing/mobile/vital-sign-trend/' + patientId, method: 'get', params })
|
||||
}
|
||||
Reference in New Issue
Block a user