feat: 评估趋势分析 + 影像历史对比页面增强

- 评估趋势分析页面增强(统计卡片/趋势图表/风险分布/评估记录明细)
- 影像历史对比页面增强(统计卡片/新增/详情/筛选)
This commit is contained in:
2026-06-07 20:47:15 +08:00
parent 20354e8e19
commit 98fbc4ddf9
2 changed files with 327 additions and 28 deletions

View File

@@ -1,27 +1,179 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">评估趋势</span></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">护理评估趋势分析</span>
<div>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</div>
<!-- 查询表单 -->
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="就诊ID"><el-input v-model="encounterId" style="width:120px"/></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
<el-form-item label="就诊"><el-input v-model="encounterId" placeholder="请输入就诊号" style="width:180px"/></el-form-item>
<el-form-item label="评估类型">
<el-select v-model="assessmentType" clearable placeholder="请选择" style="width:140px">
<el-option label="压疮评估(Braden)" value="BRADEN"/>
<el-option label="跌倒评估(Morse)" value="MORSE"/>
<el-option label="营养筛查(NRS2002)" value="NRS2002"/>
<el-option label="疼痛评估" value="PAIN"/>
<el-option label="管道风险" value="TUBE"/>
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" style="width:240px"/>
</el-form-item>
</el-form>
</el-card>
<el-table :data="trendData" border stripe>
<el-table-column prop="assessmentType" label="评估类型" width="110"/>
<el-table-column prop="score" label="评分" width="80" align="center"/>
<el-table-column prop="riskLevel" label="风险等级" width="90">
<template #default="{row}"><el-tag :type="{HIGH:'danger',MEDIUM:'warning',LOW:'success'}[row.riskLevel]" size="small">{{ row.riskLevel }}</el-tag></template>
</el-table-column>
<el-table-column prop="assessTime" label="评估时间" width="170"/>
<el-table-column prop="assessorName" label="评估人" width="90"/>
</el-table>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 趋势图表区域 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="12">
<el-card shadow="never">
<template #header>评估评分趋势</template>
<div style="height:300px;display:flex;align-items:center;justify-content:center;flex-direction:column">
<div v-if="trendData.length > 0" style="width:100%;padding:20px">
<div v-for="(item, index) in trendData.slice(0, 10)" :key="index" style="display:flex;align-items:center;margin-bottom:8px">
<span style="width:80px;font-size:12px;color:#666">{{ item.assessmentType }}</span>
<div style="flex:1;background:#f0f0f0;border-radius:4px;height:20px">
<div :style="{width: Math.min(item.score * 10, 100) + '%', background: item.riskLevel === 'HIGH' ? '#F56C6C' : item.riskLevel === 'MEDIUM' ? '#E6A23C' : '#67C23A', height: '100%', borderRadius: '4px'}"></div>
</div>
<span style="width:40px;text-align:right;font-size:12px;font-weight:bold">{{ item.score }}</span>
</div>
</div>
<el-empty v-else description="暂无数据"/>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>风险等级分布</template>
<div style="height:300px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px">
<div v-for="(item, index) in riskDistribution" :key="index" style="display:flex;align-items:center;gap:8px">
<el-tag :type="item.type" size="small" style="width:60px;text-align:center">{{ item.level }}</el-tag>
<div style="width:200px;background:#f0f0f0;border-radius:4px;height:24px">
<div :style="{width: item.percent + '%', background: item.color, height: '100%', borderRadius: '4px', transition: 'width 0.5s'}"></div>
</div>
<span style="font-size:13px;font-weight:bold">{{ item.count }} ({{ item.percent }}%)</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 评估记录表 -->
<el-card shadow="never">
<template #header>评估记录明细</template>
<el-table :data="trendData" border stripe v-loading="loading">
<el-table-column type="index" label="序号" width="60" align="center"/>
<el-table-column prop="assessmentType" label="评估类型" width="120" align="center">
<template #default="{row}">
<el-tag size="small">{{ assessmentTypeText(row.assessmentType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="score" label="评分" width="80" align="center">
<template #default="{row}">
<span :style="{color: row.riskLevel === 'HIGH' ? '#F56C6C' : row.riskLevel === 'MEDIUM' ? '#E6A23C' : '#67C23A', fontWeight:'bold', fontSize:'16px'}">
{{ row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="riskLevel" label="风险等级" width="100" align="center">
<template #default="{row}">
<el-tag :type="{HIGH:'danger',MEDIUM:'warning',LOW:'success',NORMAL:'info'}[row.riskLevel]" size="small">
{{ riskLevelText(row.riskLevel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessTime" label="评估时间" width="170"/>
<el-table-column prop="assessorName" label="评估人" width="100"/>
<el-table-column prop="detail" label="评估备注" min-width="150" show-overflow-tooltip/>
</el-table>
</el-card>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {ref, computed, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getTrend} from './api'
const encounterId=ref('')
const trendData=ref([])
const loadData=async()=>{if(!encounterId.value)return;const r=await getTrend({encounterId:encounterId.value});trendData.value=r.data||[]}
const encounterId = ref('')
const assessmentType = ref('')
const dateRange = ref(null)
const loading = ref(false)
const trendData = ref([])
const statCards = ref([
{label:'总评估数', value:0, color:'#409eff'},
{label:'高危患者', value:0, color:'#f56c6c'},
{label:'中危患者', value:0, color:'#e6a23c'},
{label:'低危患者', value:0, color:'#67c23a'},
{label:'平均分', value:0, color:'#909399'},
{label:'评估类型', value:0, color:'#409eff'}
])
const riskDistribution = computed(() => {
let high = 0, medium = 0, low = 0, normal = 0
trendData.value.forEach(row => {
if (row.riskLevel === 'HIGH') high++
else if (row.riskLevel === 'MEDIUM') medium++
else if (row.riskLevel === 'LOW') low++
else normal++
})
const total = trendData.value.length || 1
return [
{level:'高危', count:high, percent:Math.round(high*100/total), type:'danger', color:'#F56C6C'},
{level:'中危', count:medium, percent:Math.round(medium*100/total), type:'warning', color:'#E6A23C'},
{level:'低危', count:low, percent:Math.round(low*100/total), type:'success', color:'#67C23A'},
{level:'正常', count:normal, percent:Math.round(normal*100/total), type:'info', color:'#909399'}
]
})
function assessmentTypeText(t) {
return {BRADEN:'压疮评估',MORSE:'跌倒评估',NRS2002:'营养筛查',PAIN:'疼痛评估',TUBE:'管道风险'}[t] || t
}
function riskLevelText(l) {
return {HIGH:'高危',MEDIUM:'中危',LOW:'低危',NORMAL:'正常'}[l] || l
}
async function loadData() {
if (!encounterId.value) return
loading.value = true
try {
const r = await getTrend({encounterId:encounterId.value, assessmentType:assessmentType.value})
trendData.value = r.data || []
// Stats
let high = 0, medium = 0, low = 0, totalScore = 0, typeSet = new Set()
trendData.value.forEach(row => {
if (row.riskLevel === 'HIGH') high++
else if (row.riskLevel === 'MEDIUM') medium++
else low++
totalScore += (row.score || 0)
typeSet.add(row.assessmentType)
})
statCards.value[0].value = trendData.value.length
statCards.value[1].value = high
statCards.value[2].value = medium
statCards.value[3].value = low
statCards.value[4].value = trendData.value.length > 0 ? (totalScore / trendData.value.length).toFixed(1) : 0
statCards.value[5].value = typeSet.size
} finally { loading.value = false }
}
function exportReport() { ElMessage.info('导出功能开发中') }
</script>

View File

@@ -1,27 +1,174 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">影像历史对比</span></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">影像历史对比</span>
<div>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button type="success" @click="showAdd = true">新增检查</el-button>
</div>
</div>
<!-- 查询表单 -->
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="患者ID"><el-input v-model="patientId" style="width:120px"/></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
<el-form-item label="患者ID"><el-input v-model="patientId" placeholder="请输入患者ID" style="width:180px"/></el-form-item>
<el-form-item label="检查类型">
<el-select v-model="examType" clearable placeholder="请选择" style="width:140px">
<el-option label="X光" value="XRAY"/>
<el-option label="CT" value="CT"/>
<el-option label="MRI" value="MRI"/>
<el-option label="超声" value="ULTRASOUND"/>
<el-option label="内镜" value="ENDOSCOPY"/>
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" style="width:240px"/>
</el-form-item>
</el-form>
</el-card>
<el-table :data="imageData" border stripe>
<el-table-column prop="examinationType" label="检查类型" width="100"/>
<el-table-column prop="examinationName" label="检查名称" width="150"/>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table :data="imageData" border stripe v-loading="loading">
<el-table-column type="index" label="序号" width="60" align="center"/>
<el-table-column prop="examinationType" label="检查类型" width="100" align="center">
<template #default="{row}">
<el-tag size="small" :type="examTagType(row.examinationType)">
{{ examTypeText(row.examinationType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="examinationName" label="检查名称" width="150" show-overflow-tooltip/>
<el-table-column prop="bodyPart" label="部位" width="100"/>
<el-table-column prop="findingText" label="所见" min-width="200" show-overflow-tooltip/>
<el-table-column prop="conclusionText" label="结论" min-width="200" show-overflow-tooltip/>
<el-table-column prop="examinationDate" label="检查日期" width="120"/>
<el-table-column prop="doctorName" label="医生" width="90"/>
<el-table-column prop="doctorName" label="医生" width="100"/>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增弹窗 -->
<el-dialog title="新增影像检查记录" v-model="showAdd" width="650px" append-to-body>
<el-form :model="formData" label-width="100px">
<el-form-item label="检查类型" required>
<el-select v-model="formData.examinationType" placeholder="请选择">
<el-option label="X光" value="XRAY"/>
<el-option label="CT" value="CT"/>
<el-option label="MRI" value="MRI"/>
<el-option label="超声" value="ULTRASOUND"/>
<el-option label="内镜" value="ENDOSCOPY"/>
</el-select>
</el-form-item>
<el-form-item label="检查名称" required><el-input v-model="formData.examinationName" placeholder="如: 胸部CT平扫"/></el-form-item>
<el-form-item label="检查部位"><el-input v-model="formData.bodyPart" placeholder="如: 胸部、腹部"/></el-form-item>
<el-form-item label="所见"><el-input v-model="formData.findingText" type="textarea" :rows="3" placeholder="请输入影像所见"/></el-form-item>
<el-form-item label="结论"><el-input v-model="formData.conclusionText" type="textarea" :rows="2" placeholder="请输入影像结论"/></el-form-item>
<el-form-item label="检查医生"><el-input v-model="formData.doctorName" placeholder="请输入医生姓名"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="影像检查详情" v-model="detailVisible" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="检查类型">{{ examTypeText(detailData.examinationType) }}</el-descriptions-item>
<el-descriptions-item label="检查名称">{{ detailData.examinationName }}</el-descriptions-item>
<el-descriptions-item label="部位">{{ detailData.bodyPart }}</el-descriptions-item>
<el-descriptions-item label="检查日期">{{ detailData.examinationDate }}</el-descriptions-item>
<el-descriptions-item label="医生">{{ detailData.doctorName }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top:16px">
<h4>所见</h4>
<div style="padding:12px;background:#f5f7fa;border-radius:4px;min-height:60px;white-space:pre-wrap;">{{ detailData.findingText || '暂无' }}</div>
<h4 style="margin-top:12px">结论</h4>
<div style="padding:12px;background:#f5f7fa;border-radius:4px;min-height:60px;white-space:pre-wrap;">{{ detailData.conclusionText || '暂无' }}</div>
</div>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {compareImages} from './api'
const patientId=ref('')
const imageData=ref([])
const loadData=async()=>{if(!patientId.value)return;const r=await compareImages({patientId:patientId.value});imageData.value=r.data||[]}
import {ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {compareImages, addRecord} from './api'
const loading = ref(false)
const imageData = ref([])
const patientId = ref('')
const examType = ref('')
const dateRange = ref(null)
const showAdd = ref(false)
const detailVisible = ref(false)
const detailData = ref({})
const statCards = ref([
{label:'总检查数', value:0, color:'#409eff'},
{label:'X光', value:0, color:'#67c23a'},
{label:'CT', value:0, color:'#e6a23c'},
{label:'MRI', value:0, color:'#f56c6c'},
{label:'超声', value:0, color:'#909399'},
{label:'异常率', value:'0%', color:'#409eff'}
])
const formData = ref({
examinationType:'CT', examinationName:'', bodyPart:'', findingText:'', conclusionText:'', doctorName:''
})
function examTypeText(t) {
return {XRAY:'X光',CT:'CT',MRI:'MRI',ULTRASOUND:'超声',ENDOSCOPY:'内镜'}[t] || t
}
function examTagType(t) {
return {XRAY:'info',CT:'warning',MRI:'danger',ULTRASOUND:'success',ENDOSCOPY:''}[t] || 'info'
}
async function loadData() {
if (!patientId.value) return
loading.value = true
try {
const r = await compareImages({patientId:patientId.value, examType:examType.value})
imageData.value = r.data || []
// Stats
let counts = {XRAY:0, CT:0, MRI:0, ULTRASOUND:0, ENDOSCOPY:0}
imageData.value.forEach(row => { if (counts[row.examinationType] !== undefined) counts[row.examinationType]++ })
statCards.value[0].value = imageData.value.length
statCards.value[1].value = counts.XRAY
statCards.value[2].value = counts.CT
statCards.value[3].value = counts.MRI
statCards.value[4].value = counts.ULTRASOUND
statCards.value[5].value = '0%'
} finally { loading.value = false }
}
function handleDetail(row) {
detailData.value = row
detailVisible.value = true
}
async function submitForm() {
await addRecord(formData.value)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
onMounted(() => {})
</script>