添加动态血压相关

This commit is contained in:
Flow 2025-07-14 15:38:43 +08:00
parent 9af33c70db
commit c1911aee14
4 changed files with 863 additions and 0 deletions

52
src/api/abpm/index.ts Normal file
View File

@ -0,0 +1,52 @@
import request from '@/config/axios'
// ABPM动态血压监测 VO
export interface abpmVO {
id: number // 主键ID
examid: string // 检查ID
regid: string // 患者注册ID
orgid: string // 机构ID
orgname: string // 机构名称
managerorg: string // 管理机构
deviceid: string // 设备ID
devicename: string // 设备名称
status: number // 状态0未申请1已申请
systolicPressure: number // 收缩压(mmHg)
diastolicPressure: number // 舒张压(mmHg)
heartRate: number // 心率(次/分)
measureTime: Date // 测量时间
analysisResult: string // 分析结果
}
// ABPM动态血压监测 API
export const abpmApi = {
// 查询ABPM动态血压监测分页
getabpmPage: async (params: any) => {
return await request.get({ url: `/system/abpm/page`, params })
},
// 查询ABPM动态血压监测详情
getabpm: async (id: number) => {
return await request.get({ url: `/system/abpm/get?id=` + id })
},
// 新增ABPM动态血压监测
createabpm: async (data: abpmVO) => {
return await request.post({ url: `/system/abpm/create`, data })
},
// 修改ABPM动态血压监测
updateabpm: async (data: abpmVO) => {
return await request.put({ url: `/system/abpm/update`, data })
},
// 删除ABPM动态血压监测
deleteabpm: async (id: number) => {
return await request.delete({ url: `/system/abpm/delete?id=` + id })
},
// 导出ABPM动态血压监测 Excel
exportabpm: async (params) => {
return await request.download({ url: `/system/abpm/export-excel`, params })
}
}

View File

@ -0,0 +1,269 @@
<template>
<div class="abpm-analysis">
<!-- 分析弹窗组件 -->
<AnalysisDialog ref="analysisDialogRef" />
<!-- 顶部筛选区 -->
<el-card class="filter-card" shadow="never">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="姓名:">
<el-input
v-model="filters.name"
placeholder="请输入姓名"
clearable
style="width: 120px"
/>
</el-form-item>
<el-form-item label="性别:">
<el-select v-model="filters.gender" placeholder="请选择" clearable style="width: 100px">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="日期:">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="佩戴时间:">
<el-date-picker
v-model="filters.wearTime"
type="datetime"
placeholder="选择时间"
style="width: 180px"
value-format="YYYY-MM-DD HH:mm"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery" :loading="loading">
<Icon icon="ep:search" />搜索
</el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" />重置 </el-button>
<el-button type="primary" @click="handleAddPatient">
<Icon icon="ep:plus" />新增患者
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格区 -->
<el-card class="table-card" shadow="never" style="margin-top: 16px">
<div style="margin-bottom: 16px; color: #666">
共找到 <span style="color: #409eff; font-weight: bold">{{ tableData.length }}</span> 条记录
</div>
<el-table :data="tableData" border stripe style="width: 100%">
<el-table-column type="index" label="序号" align="center" min-width="60" />
<el-table-column prop="name" label="姓名" align="center" min-width="90" />
<el-table-column prop="gender" label="性别" align="center" min-width="60" />
<el-table-column prop="age" label="年龄" align="center" min-width="60" />
<el-table-column prop="wearTime" label="佩戴时间" align="center" min-width="120" />
<el-table-column prop="device" label="设备" align="center" min-width="150">
<template #default="{ row, $index }">
<!-- 编辑状态 -->
<div v-if="row.editingDevice" style="display: flex; align-items: center; gap: 8px">
<el-select
v-model="row.device"
placeholder="选择设备"
size="small"
style="width: 110px"
clearable
>
<el-option
v-for="item in deviceList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" size="small" @click="saveDevice(row, $index)" link>
<Icon icon="ep:check" />
</el-button>
<el-button size="small" @click="cancelEditDevice(row, $index)" link>
<Icon icon="ep:close" />
</el-button>
</div>
<!-- 显示状态 -->
<div
v-else
style="display: flex; align-items: center; justify-content: center; gap: 8px"
>
<span>{{ getDeviceLabel(row.device) }}</span>
<el-button type="primary" size="small" @click="editDevice(row, $index)" link>
<Icon icon="ep:edit" />
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="org" label="机构" align="center" min-width="120" />
<el-table-column label="分析" align="center" min-width="70">
<template #default="{ row }">
<el-link type="primary" @click="onAnalyze(row)">分析</el-link>
</template>
</el-table-column>
<el-table-column label="报告" align="center" min-width="70">
<template #default="{ row }">
<el-link type="primary" @click="onViewReport(row)">查看</el-link>
</template>
</el-table-column>
<el-table-column label="申请" align="center" min-width="70">
<template #default>
<el-link type="success" :underline="false" disabled>已申请</el-link>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import AnalysisDialog from './analysis.vue'
const loading = ref(false)
const analysisDialogRef = ref()
const filters = ref({
name: '',
gender: '',
dateRange: [],
wearTime: '',
device: ''
})
//
const deviceList = ref<any[]>([])
//
const fetchDeviceList = () => {
setTimeout(() => {
deviceList.value = [
{ label: '动态血压-1001', value: '1001' },
{ label: '动态血压-1002', value: '1002' },
{ label: '动态血压-1003', value: '1003' }
]
}, 500)
}
onMounted(() => {
fetchDeviceList()
})
// tableData value
const tableData = ref([
{
name: '张晓宁',
gender: '男',
age: 45,
createdAt: '2025/7/09',
wearTime: '2025/7/14 11:23',
device: '1001',
org: '廊坊华康',
editingDevice: false
},
{
name: '李小明',
gender: '女',
age: 32,
createdAt: '2025/7/10',
wearTime: '2025/7/15 09:30',
device: '1002',
org: '北京协和',
editingDevice: false
},
{
name: '王大力',
gender: '男',
age: 58,
createdAt: '2025/7/11',
wearTime: '2025/7/16 14:15',
device: '1001',
org: '上海瑞金',
editingDevice: false
}
])
// label
const getDeviceLabel = (value: string) => {
const found = deviceList.value.find((item) => item.value === value)
return found ? found.label : value
}
const handleQuery = () => {
loading.value = true
// API
setTimeout(() => {
ElMessage.success('查询功能待接入')
loading.value = false
}, 300)
}
const resetQuery = () => {
filters.value = { name: '', gender: '', dateRange: [], wearTime: '', device: '' }
ElMessage.info('已清除筛选条件')
}
const handleAddPatient = () => {
ElMessage.success('新增患者功能待开发')
}
//
const editDevice = (row: any, index: number) => {
//
row.originalDevice = row.device
row.editingDevice = true
}
const saveDevice = (row: any, index: number) => {
row.editingDevice = false
delete row.originalDevice
ElMessage.success(`已更新设备为:${getDeviceLabel(row.device)}`)
}
const cancelEditDevice = (row: any, index: number) => {
//
if (row.originalDevice !== undefined) {
row.device = row.originalDevice
}
row.editingDevice = false
delete row.originalDevice
}
const onAnalyze = (row: any) => {
//
analysisDialogRef.value?.open()
}
const onViewReport = (row: any) => {
ElMessage.success(`查看报告:${row.name}`)
}
const onSetting = (row: any) => {
ElMessage.success(`设置:${row.name}`)
}
</script>
<style scoped>
.abpm-analysis {
padding: 24px;
background: #f6f8fa;
min-height: 100vh;
}
.filter-card {
margin-bottom: 0;
background: #fff;
}
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 16px;
}
.table-card {
background: #fff;
}
</style>

View File

@ -0,0 +1,542 @@
<template>
<el-dialog
v-model="visible"
:fullscreen="true"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="true"
class="abpm-analysis-dialog"
>
<el-card class="abpm-analysis" shadow="never">
<!-- 个人信息指标与统计信息全部合并到一个info-section -->
<div class="info-section">
<div class="info-item">
<span class="label">姓名</span>
<span class="value">张晓宇</span>
</div>
<div class="info-item">
<span class="label">性别</span>
<span class="value"></span>
</div>
<div class="info-item">
<span class="label">年龄</span>
<span class="value">45</span>
</div>
<div class="info-item">
<span class="label">记录开始时间</span>
<span class="value">2025-07-09 10:31</span>
</div>
<div class="info-item">
<span class="label">记录结束时间</span>
<span class="value">2025-07-10 09:31</span>
</div>
<div class="info-item">
<span class="label">记录总时长</span>
<span class="value">23:00</span>
</div>
<div class="info-item">
<span class="label">成功读取次数</span>
<span class="value">39</span>
</div>
<div class="info-item">
<span class="label">成功读取百分比</span>
<span class="value">100%</span>
</div>
<div class="info-item">
<span class="label">最高收缩压</span>
<span class="value">245 mmHg</span>
<span class="desc">09 20:02</span>
</div>
<div class="info-item">
<span class="label">最低收缩压</span>
<span class="value">194 mmHg</span>
<span class="desc">09 20:02</span>
</div>
<div class="info-item">
<span class="label">血压变异系数</span>
<span class="value">20.89%</span>
</div>
<div class="info-item">
<span class="label">舒张压变异系数</span>
<span class="value">28.16%</span>
</div>
<div class="info-item">
<span class="label">最低舒张压</span>
<span class="value">76 mmHg</span>
<span class="desc">09 18:42</span>
</div>
<div class="info-item">
<span class="label">平均收缩压</span>
<span class="value">151</span>
</div>
<div class="info-item">
<span class="label">平均舒张压</span>
<span class="value">106</span>
</div>
<div class="info-item">
<span class="label">血压高峰概率</span>
<span class="value">56.41%</span>
</div>
</div>
<!-- 血压趋势图 -->
<el-card class="chart-section" shadow="never">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
"
>
<div class="chart-title" style="margin-bottom: 0">血压趋势图</div>
<div style="display: flex; align-items: center">
<span style="margin-right: 12px; font-size: 15px; color: #333">显示模式</span>
<el-select
v-model="chartMode"
size="small"
style="width: 160px"
@change="updateChartSeries"
>
<el-option label="血压和心率" value="all" />
<el-option label="血压" value="bp" />
<el-option label="心率" value="hr" />
</el-select>
</div>
</div>
<div class="chart-container">
<Echart v-if="showChart" :options="chartOption" :height="350" />
<div v-else class="chart-loading">
<el-icon style="margin-right: 8px"><Loading /></el-icon>
图表加载中...
</div>
</div>
</el-card>
<!-- 分析结果 -->
<el-card class="result-section" shadow="never">
<div class="result-title">分析结果</div>
<div class="result-content">
最高收缩压245mmHg最低收缩压194mmHg发生于09日 20:02 最高舒张压119mmHg发生于09日
10:31最低舒张压76mmHg发生于09日 18:42
平均收缩压151mmHg理想&lt;130mmHg平均舒张压106mmHg理想&lt;80
平均脉压120mmHg平均心率106次/
血压高峰概率56.41%血压高峰概率66.67%夜间血压下降率4.63%
</div>
</el-card>
<!-- 操作按钮 -->
<div class="action-section">
<el-button type="primary">保存</el-button>
<el-button @click="handleClose">关闭</el-button>
<el-button type="success">报告浏览</el-button>
</div>
</el-card>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, defineExpose, nextTick } from 'vue'
import type { EChartsOption } from 'echarts'
import { Echart } from '@/components/Echart'
import { Loading } from '@element-plus/icons-vue'
//
const visible = ref(false)
// DOM
const showChart = ref(false)
//
const chartMode = ref<'all' | 'bp' | 'hr'>('all')
//
const open = () => {
visible.value = true
showChart.value = false
//
nextTick(() => {
setTimeout(() => {
showChart.value = true
// DOM
setTimeout(() => {
updateChartSeries()
}, 100)
}, 800) //
})
}
//
const close = () => {
visible.value = false
showChart.value = false
}
//
const handleClose = () => {
close()
}
//
defineExpose({
open,
close
})
// mock
const allSeries = [
{
name: '收缩压统计',
type: 'line' as const,
smooth: false,
data: [
120, 130, 125, 140, 135, 150, 160, 245, 200, 210, 220, 180, 170, 160, 150, 140, 130, 120, 125,
130, 140, 150, 200
],
itemStyle: {
color: '#409eff'
},
lineStyle: {
width: 2
}
},
{
name: '舒张压统计',
type: 'line' as const,
smooth: false,
data: [
80, 85, 90, 95, 100, 105, 110, 119, 100, 110, 115, 100, 95, 90, 85, 80, 76, 80, 85, 90, 95,
100, 110
],
itemStyle: {
color: '#67c23a'
},
lineStyle: {
width: 2
}
},
{
name: '心率统计',
type: 'line' as const,
smooth: false,
data: [
70, 72, 75, 78, 80, 82, 85, 90, 88, 86, 84, 82, 80, 78, 76, 74, 72, 70, 71, 73, 75, 77, 79
],
itemStyle: {
color: '#e6a23c'
},
lineStyle: {
width: 2
}
}
]
const chartOption = ref<any>({
grid: {
left: 60,
right: 40,
bottom: 60,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
backgroundColor: 'rgba(50,50,50,0.7)',
borderWidth: 0,
textStyle: {
color: '#fff'
}
},
legend: {
data: ['收缩压统计', '舒张压统计', '心率统计'],
top: 10,
left: 'center'
},
xAxis: {
type: 'category',
data: [
'09日11:00',
'09日12:00',
'09日13:00',
'09日14:00',
'09日15:00',
'09日16:00',
'09日17:00',
'09日18:00',
'09日19:00',
'09日20:00',
'09日21:00',
'09日22:00',
'09日23:00',
'10日00:00',
'10日01:00',
'10日02:00',
'10日03:00',
'10日04:00',
'10日05:00',
'10日06:00',
'10日07:00',
'10日08:00',
'10日09:00'
],
axisTick: {
show: false
},
axisLabel: {
color: '#666',
rotate: 45
},
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
}
},
yAxis: {
type: 'value',
min: 0,
max: 260,
axisTick: {
show: false
},
axisLabel: {
color: '#666'
},
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
},
series: allSeries
})
function updateChartSeries() {
//
if (!showChart.value) return
let newSeries: any[] = []
let newLegendData: string[] = []
if (chartMode.value === 'all') {
newSeries = [...allSeries]
newLegendData = ['收缩压统计', '舒张压统计', '心率统计']
} else if (chartMode.value === 'bp') {
newSeries = allSeries.slice(0, 2)
newLegendData = ['收缩压统计', '舒张压统计']
} else if (chartMode.value === 'hr') {
newSeries = [allSeries[2]]
newLegendData = ['心率统计']
}
//
chartOption.value = {
...chartOption.value,
series: newSeries,
legend: {
...chartOption.value.legend,
data: newLegendData
}
}
}
</script>
<style scoped>
.abpm-analysis-dialog {
:deep(.el-dialog__body) {
padding: 0;
height: calc(100vh - 60px);
overflow-y: auto;
}
:deep(.el-dialog) {
z-index: 2000;
margin: 0;
width: 100% !important;
height: 100% !important;
}
:deep(.el-dialog__header) {
padding: 16px 24px;
border-bottom: 1px solid #e4e7ed;
}
}
.abpm-analysis {
width: 100%;
max-width: none;
margin: 0;
}
.info-section {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.info-item {
display: flex;
align-items: center;
font-size: 14px;
color: #333;
}
.info-item .label {
color: #666;
margin-right: 8px;
white-space: nowrap;
}
.info-item .value {
font-weight: 500;
color: #333;
}
.indicator-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.indicator-card {
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: 1px solid #ebeef5;
transition: all 0.3s ease;
}
.indicator-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.indicator-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.indicator-value {
font-size: 24px;
font-weight: bold;
color: #409eff;
margin-bottom: 4px;
}
.indicator-time {
font-size: 12px;
color: #999;
}
.unit {
font-size: 14px;
color: #666;
}
.chart-section {
margin-bottom: 20px;
:deep(.echart) {
width: 100% !important;
height: 350px !important;
}
:deep(.echart div) {
width: 100% !important;
height: 350px !important;
}
}
.chart-container {
width: 100%;
height: 350px;
min-height: 350px;
position: relative;
}
.chart-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 14px;
}
.chart-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.result-section {
margin-bottom: 20px;
}
.result-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.result-content {
font-size: 15px;
color: #333;
line-height: 1.8;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.action-section {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid #e4e7ed;
}
/* desc样式补充 */
.info-item .desc {
color: #999;
font-size: 12px;
margin-left: 6px;
}
@media (max-width: 768px) {
.info-section {
grid-template-columns: 1fr;
gap: 8px;
}
.indicator-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.indicator-card {
height: 90px;
}
.indicator-value {
font-size: 20px;
}
}
</style>