feat(mobile): 移动护理APP医嘱执行+生命体征

This commit is contained in:
2026-06-18 22:47:26 +08:00
parent aa4a582981
commit cb9968ee76
15 changed files with 1419 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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',

View 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>

View 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>

View 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>

View 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>

View 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 })
}