feat(mobile): 移动护理APP医嘱执行+生命体征
This commit is contained in:
@@ -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