修改动态血氧功能模版
This commit is contained in:
parent
dc83dbe497
commit
1cba193c9f
527
public/spo2-report-template.html
Normal file
527
public/spo2-report-template.html
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>动态血氧监测报告</title>
|
||||||
|
<style>
|
||||||
|
body { background: #f5f7fa; margin: 0; font-family: 'Microsoft YaHei', Arial, sans-serif; }
|
||||||
|
.spo2-report-template { max-width: 900px; margin: 0 auto; padding: 0; }
|
||||||
|
.report-page { background: white; margin: 0 auto 40px auto; max-width: 900px; min-height: 900px; box-shadow: 0 2px 16px rgba(0,0,0,0.08); border-radius: 12px; padding: 48px 56px 32px 56px; page-break-after: always; }
|
||||||
|
.report-title { text-align: center; font-size: 28px; font-weight: bold; margin-bottom: 32px; }
|
||||||
|
.report-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 32px; font-size: 16px; margin-bottom: 32px; }
|
||||||
|
.report-info-item { display: flex; }
|
||||||
|
.report-info-label { display: inline-block; min-width: 80px; flex-shrink: 0; }
|
||||||
|
.report-info-value { font-weight: 500; word-break: break-all; }
|
||||||
|
.report-info-item.full-width { grid-column: span 2; }
|
||||||
|
.stat-section { margin-bottom: 24px; }
|
||||||
|
.stat-row { display: flex; flex-wrap: wrap; gap: 20px 60px; font-size: 18px; font-weight: 500; }
|
||||||
|
.stat-item { white-space: nowrap; min-width: 180px; }
|
||||||
|
.stat-value { color: #409eff; font-weight: bold; margin-left: 4px; }
|
||||||
|
.distribution-section { margin-bottom: 32px; }
|
||||||
|
.distribution-table { width: 100%; border-collapse: collapse; margin-top: 12px; margin-bottom: 12px; }
|
||||||
|
.distribution-table th, .distribution-table td { border: 1px solid #e4e7ed; padding: 8px 12px; text-align: center; }
|
||||||
|
.data-table-section { margin-bottom: 32px; }
|
||||||
|
.extreme-table { width: 100%; border-collapse: collapse; margin-top: 12px; margin-bottom: 12px; }
|
||||||
|
.extreme-table th, .extreme-table td { border: 1px solid #e4e7ed; padding: 8px 12px; text-align: center; }
|
||||||
|
.diagnosis-content { margin: 48px 0 32px 0; padding: 32px; background: #f8f9fa; border-radius: 8px; min-height: 300px; }
|
||||||
|
.diagnosis-text { font-size: 18px; white-space: pre-wrap; color: #303133; font-family: inherit; }
|
||||||
|
.report-footer { display: flex; justify-content: space-between; margin-top: 80px; font-size: 16px; }
|
||||||
|
.pie-charts-group { display: flex; gap: 32px; justify-content: center; margin-top: 32px; }
|
||||||
|
.pie-chart-block { display: flex; flex-direction: column; align-items: center; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); padding: 24px 16px; min-width: 260px; }
|
||||||
|
.pie-chart-title { font-size: 18px; font-weight: bold; margin-bottom: 12px; }
|
||||||
|
.pie-chart-row { position: relative; }
|
||||||
|
.pie-center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
|
||||||
|
.pie-center-value { font-size: 20px; font-weight: bold; color: #303133; }
|
||||||
|
.pie-center-label { font-size: 12px; color: #909399; }
|
||||||
|
.pie-legend { display: flex; flex-direction: column; }
|
||||||
|
.pie-legend-item { display: flex; align-items: center; margin-bottom: 8px; font-size: 14px; }
|
||||||
|
.pie-legend-color { width: 14px; height: 14px; border-radius: 2px; margin-right: 8px; }
|
||||||
|
.pie-legend-label { width: 40px; }
|
||||||
|
.pie-legend-range { width: 60px; color: #606266; }
|
||||||
|
.pie-legend-value { margin-left: 8px; font-weight: 500; }
|
||||||
|
#loading-mask { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.85); z-index: 9999; display: flex; align-items: center; justify-content: center; font-size: 22px; color: #409eff; letter-spacing: 2px; transition: opacity 0.3s; }
|
||||||
|
#print-button { position: fixed; top: 20px; right: 20px; background-color: #409eff; color: white; border: none; border-radius: 4px; padding: 8px 16px; font-size: 16px; cursor: pointer; display: flex; align-items: center; box-shadow: 0 2px 6px rgba(0,0,0,0.15); z-index: 100; transition: all 0.2s; }
|
||||||
|
#print-button:hover { background-color: #337ecc; }
|
||||||
|
#print-button svg { margin-right: 6px; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { background: white; }
|
||||||
|
.report-page { box-shadow: none; margin: 0; padding: 20px; page-break-after: always; }
|
||||||
|
#print-button, #loading-mask { display: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading-mask">加载中...</div>
|
||||||
|
<button id="print-button" onclick="window.print()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
||||||
|
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
||||||
|
<rect x="6" y="14" width="12" height="8"></rect>
|
||||||
|
</svg>
|
||||||
|
打印报告
|
||||||
|
</button>
|
||||||
|
<div class="spo2-report-template">
|
||||||
|
<div class="report-page">
|
||||||
|
<h2 class="report-title">动态血氧监测报告</h2>
|
||||||
|
<div class="report-info-grid">
|
||||||
|
<div class="report-info-item"><span class="report-info-label">姓名:</span><span class="report-info-value patient-name">--</span></div>
|
||||||
|
<div class="report-info-item"><span class="report-info-label">性别:</span><span class="report-info-value patient-gender">--</span></div>
|
||||||
|
<div class="report-info-item"><span class="report-info-label">年龄:</span><span class="report-info-value patient-age">--</span></div>
|
||||||
|
<div class="report-info-item"><span class="report-info-label">检查日期:</span><span class="report-info-value exam-date">--</span></div>
|
||||||
|
<div class="report-info-item"><span class="report-info-label">设备:</span><span class="report-info-value device-name">--</span></div>
|
||||||
|
<div class="report-info-item full-width"><span class="report-info-label">检查ID:</span><span class="report-info-value exam-id">--</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-section">
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-item">平均血氧饱和度:<span class="stat-value average-spo2">--</span>%</div>
|
||||||
|
<div class="stat-item">最低血氧饱和度:<span class="stat-value min-spo2">--</span>%</div>
|
||||||
|
<div class="stat-item">最高血氧饱和度:<span class="stat-value max-spo2">--</span>%</div>
|
||||||
|
<div class="stat-item">低氧时间:<span class="stat-value low-oxygen-time">--</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="distribution-section">
|
||||||
|
<h3>血氧饱和度分布统计</h3>
|
||||||
|
<table class="distribution-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>时间段</th>
|
||||||
|
<th>优秀 (≥95%)</th>
|
||||||
|
<th>良好 (90-94%)</th>
|
||||||
|
<th>偏低 (85-89%)</th>
|
||||||
|
<th>危险 (<85%)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>全天</td>
|
||||||
|
<td class="all-excellent">--</td>
|
||||||
|
<td class="all-good">--</td>
|
||||||
|
<td class="all-warning">--</td>
|
||||||
|
<td class="all-danger">--</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>白天</td>
|
||||||
|
<td class="day-excellent">--</td>
|
||||||
|
<td class="day-good">--</td>
|
||||||
|
<td class="day-warning">--</td>
|
||||||
|
<td class="day-danger">--</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>夜间</td>
|
||||||
|
<td class="night-excellent">--</td>
|
||||||
|
<td class="night-good">--</td>
|
||||||
|
<td class="night-warning">--</td>
|
||||||
|
<td class="night-danger">--</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="data-table-section">
|
||||||
|
<h3>极值统计</h3>
|
||||||
|
<table class="extreme-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>血氧值</th>
|
||||||
|
<th>发生时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="color: #e53e3e; font-weight: bold">最低血氧值</td>
|
||||||
|
<td style="color: #e53e3e; font-weight: bold" class="min-spo2-value">--</td>
|
||||||
|
<td class="min-spo2-time">--</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color: #2563eb; font-weight: bold">最高血氧值</td>
|
||||||
|
<td style="color: #2563eb; font-weight: bold" class="max-spo2-value">--</td>
|
||||||
|
<td class="max-spo2-time">--</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-page report-diagnosis">
|
||||||
|
<h2 class="report-title">诊断结论</h2>
|
||||||
|
<div class="diagnosis-content">
|
||||||
|
<pre class="diagnosis-text">--</pre>
|
||||||
|
</div>
|
||||||
|
<div class="report-footer">
|
||||||
|
<div>报告医生:________________</div>
|
||||||
|
<div>报告日期:<span class="report-date">--</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-page report-pie-chart">
|
||||||
|
<h2 class="report-title">血氧分布饼状图</h2>
|
||||||
|
<div class="pie-charts-group" style="flex-direction:column;align-items:center;gap:48px;">
|
||||||
|
<div class="pie-chart-block" style="width:90%;max-width:600px;padding:32px 24px;">
|
||||||
|
<div class="pie-chart-title">全天血氧分布 (24小时)</div>
|
||||||
|
<div class="pie-chart-row" style="display:flex;align-items:center;justify-content:center;margin-top:16px;">
|
||||||
|
<div style="position:relative;width:200px;height:200px;">
|
||||||
|
<svg class="pie-ring all-day-pie" width="200" height="200" viewBox="0 0 200 200"></svg>
|
||||||
|
<div class="pie-center-text all-day-center">
|
||||||
|
<div class="pie-center-value">--</div>
|
||||||
|
<div class="pie-center-label">平均血氧</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pie-legend all-day-legend" style="margin-left:40px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;width:90%;max-width:600px;gap:24px;justify-content:center;">
|
||||||
|
<div class="pie-chart-block" style="flex:1;padding:24px 16px;">
|
||||||
|
<div class="pie-chart-title">白天血氧分布 (8:00-22:00)</div>
|
||||||
|
<div class="pie-chart-row" style="display:flex;align-items:center;justify-content:center;margin-top:16px;">
|
||||||
|
<div style="position:relative;width:160px;height:160px;">
|
||||||
|
<svg class="pie-ring daytime-pie" width="160" height="160" viewBox="0 0 160 160"></svg>
|
||||||
|
<div class="pie-center-text daytime-center">
|
||||||
|
<div class="pie-center-value">--</div>
|
||||||
|
<div class="pie-center-label">白天</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pie-legend daytime-legend" style="margin-top:16px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pie-chart-block" style="flex:1;padding:24px 16px;">
|
||||||
|
<div class="pie-chart-title">夜间血氧分布 (22:00-8:00)</div>
|
||||||
|
<div class="pie-chart-row" style="display:flex;align-items:center;justify-content:center;margin-top:16px;">
|
||||||
|
<div style="position:relative;width:160px;height:160px;">
|
||||||
|
<svg class="pie-ring nighttime-pie" width="160" height="160" viewBox="0 0 160 160"></svg>
|
||||||
|
<div class="pie-center-text nighttime-center">
|
||||||
|
<div class="pie-center-value">--</div>
|
||||||
|
<div class="pie-center-label">夜间</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pie-legend nighttime-legend" style="margin-top:16px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// loading 遮罩控制
|
||||||
|
function showLoading() { document.getElementById('loading-mask').style.display = 'flex'; }
|
||||||
|
function hideLoading() { document.getElementById('loading-mask').style.display = 'none'; }
|
||||||
|
|
||||||
|
// 数据填充主入口
|
||||||
|
function fillData(data) {
|
||||||
|
// 基本信息
|
||||||
|
document.querySelector('.patient-name').textContent = data.patientInfo?.name || '--';
|
||||||
|
document.querySelector('.patient-gender').textContent = data.patientInfo?.gender === '1' ? '男' : data.patientInfo?.gender === '2' ? '女' : '--';
|
||||||
|
document.querySelector('.patient-age').textContent = data.patientInfo?.age || '--';
|
||||||
|
document.querySelector('.exam-id').textContent = data.patientInfo?.examid || '--';
|
||||||
|
document.querySelector('.exam-date').textContent = formatDate(data.patientInfo?.weartime);
|
||||||
|
document.querySelector('.device-name').textContent = data.patientInfo?.devicename || '--';
|
||||||
|
// 统计
|
||||||
|
document.querySelector('.average-spo2').textContent = data.spo2Stats?.averageSpO2 ?? '--';
|
||||||
|
document.querySelector('.min-spo2').textContent = data.spo2Stats?.minSpO2 ?? '--';
|
||||||
|
document.querySelector('.max-spo2').textContent = data.spo2Stats?.maxSpO2 ?? '--';
|
||||||
|
document.querySelector('.low-oxygen-time').textContent = data.spo2Stats?.lowOxygenTime > 0 ? data.spo2Stats.lowOxygenTime + '分钟' : '无低氧';
|
||||||
|
// 分布
|
||||||
|
document.querySelector('.all-excellent').textContent = safeDist(data.timePeriodData?.all?.excellent);
|
||||||
|
document.querySelector('.all-good').textContent = safeDist(data.timePeriodData?.all?.good);
|
||||||
|
document.querySelector('.all-warning').textContent = safeDist(data.timePeriodData?.all?.warning);
|
||||||
|
document.querySelector('.all-danger').textContent = safeDist(data.timePeriodData?.all?.danger);
|
||||||
|
document.querySelector('.day-excellent').textContent = safeDist(data.timePeriodData?.daytime?.excellent);
|
||||||
|
document.querySelector('.day-good').textContent = safeDist(data.timePeriodData?.daytime?.good);
|
||||||
|
document.querySelector('.day-warning').textContent = safeDist(data.timePeriodData?.daytime?.warning);
|
||||||
|
document.querySelector('.day-danger').textContent = safeDist(data.timePeriodData?.daytime?.danger);
|
||||||
|
document.querySelector('.night-excellent').textContent = safeDist(data.timePeriodData?.nighttime?.excellent);
|
||||||
|
document.querySelector('.night-good').textContent = safeDist(data.timePeriodData?.nighttime?.good);
|
||||||
|
document.querySelector('.night-warning').textContent = safeDist(data.timePeriodData?.nighttime?.warning);
|
||||||
|
document.querySelector('.night-danger').textContent = safeDist(data.timePeriodData?.nighttime?.danger);
|
||||||
|
// 极值
|
||||||
|
document.querySelector('.min-spo2-value').textContent = data.spo2Stats?.minSpO2 ?? '--';
|
||||||
|
document.querySelector('.max-spo2-value').textContent = data.spo2Stats?.maxSpO2 ?? '--';
|
||||||
|
document.querySelector('.min-spo2-time').textContent = getExtremeTime('min', data.dataTableList);
|
||||||
|
document.querySelector('.max-spo2-time').textContent = getExtremeTime('max', data.dataTableList);
|
||||||
|
// 诊断结论
|
||||||
|
document.querySelector('.diagnosis-text').textContent = data.diagnosisForm?.conclusion || '--';
|
||||||
|
document.querySelector('.report-date').textContent = formatDate(new Date());
|
||||||
|
// 饼图
|
||||||
|
renderPieChartRing(data.timePeriodData?.all, document.querySelector('.all-day-pie'), document.querySelector('.all-day-legend'));
|
||||||
|
renderPieChartRing(data.timePeriodData?.daytime, document.querySelector('.daytime-pie'), document.querySelector('.daytime-legend'));
|
||||||
|
renderPieChartRing(data.timePeriodData?.nighttime, document.querySelector('.nighttime-pie'), document.querySelector('.nighttime-legend'));
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
function safeDist(obj) { return obj ? `${obj.value ?? 0} (${obj.percentage ?? 0}%)` : '--'; }
|
||||||
|
function formatDate(date) {
|
||||||
|
if (!date) return '--';
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
function getExtremeTime(type, dataTableList) {
|
||||||
|
if (!dataTableList || dataTableList.length === 0) return '--';
|
||||||
|
let targetRow = dataTableList[0];
|
||||||
|
if (type === 'min') {
|
||||||
|
targetRow = dataTableList.reduce((min, cur) => (cur.spo2value < min.spo2value ? cur : min), dataTableList[0]);
|
||||||
|
} else {
|
||||||
|
targetRow = dataTableList.reduce((max, cur) => (cur.spo2value > max.spo2value ? cur : max), dataTableList[0]);
|
||||||
|
}
|
||||||
|
return formatDateTime(targetRow.measuretime);
|
||||||
|
}
|
||||||
|
function formatDateTime(date) {
|
||||||
|
if (!date) return '--';
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
// 饼图渲染
|
||||||
|
function renderPieChartRing(data, svgElement, legendElement) {
|
||||||
|
// 颜色和标签 - 更新颜色方案,使用更鲜明的对比色
|
||||||
|
const segments = [
|
||||||
|
{ label: '优秀', color: '#10b981', range: '≥95%', value: data?.excellent?.value ?? 0, percent: data?.excellent?.percentage ?? 0 },
|
||||||
|
{ label: '良好', color: '#3b82f6', range: '90-94%', value: data?.good?.value ?? 0, percent: data?.good?.percentage ?? 0 },
|
||||||
|
{ label: '偏低', color: '#f59e0b', range: '85-89%', value: data?.warning?.value ?? 0, percent: data?.warning?.percentage ?? 0 },
|
||||||
|
{ label: '危险', color: '#ef4444', range: '<85%', value: data?.danger?.value ?? 0, percent: data?.danger?.percentage ?? 0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算平均值并更新中心文本
|
||||||
|
const centerTextElement = svgElement.parentElement.querySelector('.pie-center-text .pie-center-value');
|
||||||
|
if (centerTextElement) {
|
||||||
|
// 从数据中提取平均值
|
||||||
|
let avgValue = '--';
|
||||||
|
if (data === window.reportData?.timePeriodData?.all) {
|
||||||
|
avgValue = window.reportData?.spo2Stats?.averageSpO2 ?? '--';
|
||||||
|
} else if (data === window.reportData?.timePeriodData?.daytime) {
|
||||||
|
avgValue = window.reportData?.spo2Stats?.daytimeAverageSpO2 ?? window.reportData?.spo2Stats?.averageSpO2 ?? '--';
|
||||||
|
} else if (data === window.reportData?.timePeriodData?.nighttime) {
|
||||||
|
avgValue = window.reportData?.spo2Stats?.nighttimeAverageSpO2 ?? window.reportData?.spo2Stats?.averageSpO2 ?? '--';
|
||||||
|
}
|
||||||
|
centerTextElement.textContent = avgValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只保留有数据的分段
|
||||||
|
const nonZeroSegments = segments.filter(seg => seg.percent > 0);
|
||||||
|
svgElement.innerHTML = '';
|
||||||
|
|
||||||
|
// 环形参数
|
||||||
|
const size = svgElement.getAttribute('width');
|
||||||
|
const cx = size / 2;
|
||||||
|
const cy = size / 2;
|
||||||
|
const r = size * 0.38; // 稍微调整半径比例
|
||||||
|
const strokeWidth = size * 0.18; // 调整环形厚度
|
||||||
|
const C = 2 * Math.PI * r;
|
||||||
|
|
||||||
|
// 处理所有分段都为0的情况
|
||||||
|
if (nonZeroSegments.length === 0) {
|
||||||
|
// 画灰色圆环
|
||||||
|
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
circle.setAttribute('cx', cx);
|
||||||
|
circle.setAttribute('cy', cy);
|
||||||
|
circle.setAttribute('r', r);
|
||||||
|
circle.setAttribute('stroke', '#e5e7eb');
|
||||||
|
circle.setAttribute('stroke-width', strokeWidth);
|
||||||
|
circle.setAttribute('fill', 'none');
|
||||||
|
svgElement.appendChild(circle);
|
||||||
|
|
||||||
|
// 图例显示"暂无数据"
|
||||||
|
if (legendElement) {
|
||||||
|
legendElement.innerHTML = '<div style="color:#9ca3af;font-size:14px;padding:8px;">暂无数据</div>';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一个分段,直接画满整个圆
|
||||||
|
if (nonZeroSegments.length === 1) {
|
||||||
|
const seg = nonZeroSegments[0];
|
||||||
|
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
circle.setAttribute('cx', cx);
|
||||||
|
circle.setAttribute('cy', cy);
|
||||||
|
circle.setAttribute('r', r);
|
||||||
|
circle.setAttribute('stroke', seg.color);
|
||||||
|
circle.setAttribute('stroke-width', strokeWidth);
|
||||||
|
circle.setAttribute('fill', 'none');
|
||||||
|
svgElement.appendChild(circle);
|
||||||
|
|
||||||
|
// 添加高亮效果
|
||||||
|
const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
highlight.setAttribute('cx', cx);
|
||||||
|
highlight.setAttribute('cy', cy);
|
||||||
|
highlight.setAttribute('r', r);
|
||||||
|
highlight.setAttribute('stroke', 'white');
|
||||||
|
highlight.setAttribute('stroke-width', 2);
|
||||||
|
highlight.setAttribute('stroke-opacity', '0.4');
|
||||||
|
highlight.setAttribute('fill', 'none');
|
||||||
|
svgElement.appendChild(highlight);
|
||||||
|
|
||||||
|
// 图例只显示这一个分段
|
||||||
|
if (legendElement) {
|
||||||
|
legendElement.innerHTML = '';
|
||||||
|
renderLegendItem(legendElement, seg, 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多个分段的情况,归一化处理
|
||||||
|
const total = nonZeroSegments.reduce((sum, seg) => sum + seg.percent, 0);
|
||||||
|
const normalizedSegments = nonZeroSegments.map(seg => ({ ...seg, percent: seg.percent / total * 100 }));
|
||||||
|
|
||||||
|
// 添加背景圆环
|
||||||
|
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
bgCircle.setAttribute('cx', cx);
|
||||||
|
bgCircle.setAttribute('cy', cy);
|
||||||
|
bgCircle.setAttribute('r', r);
|
||||||
|
bgCircle.setAttribute('stroke', '#f1f5f9');
|
||||||
|
bgCircle.setAttribute('stroke-width', strokeWidth);
|
||||||
|
bgCircle.setAttribute('fill', 'none');
|
||||||
|
svgElement.appendChild(bgCircle);
|
||||||
|
|
||||||
|
// 添加白色圆形作为中心背景
|
||||||
|
const centerCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
|
centerCircle.setAttribute('cx', cx);
|
||||||
|
centerCircle.setAttribute('cy', cy);
|
||||||
|
centerCircle.setAttribute('r', r - strokeWidth/2);
|
||||||
|
centerCircle.setAttribute('fill', 'white');
|
||||||
|
svgElement.appendChild(centerCircle);
|
||||||
|
|
||||||
|
// 使用路径绘制饼图,而不是使用圆环的dash属性
|
||||||
|
let startAngle = -Math.PI / 2; // 从12点钟方向开始
|
||||||
|
|
||||||
|
normalizedSegments.forEach(seg => {
|
||||||
|
const segmentPercent = seg.percent / 100;
|
||||||
|
const endAngle = startAngle + segmentPercent * 2 * Math.PI;
|
||||||
|
|
||||||
|
// 计算路径
|
||||||
|
const x1 = cx + r * Math.cos(startAngle);
|
||||||
|
const y1 = cy + r * Math.sin(startAngle);
|
||||||
|
const x2 = cx + r * Math.cos(endAngle);
|
||||||
|
const y2 = cy + r * Math.sin(endAngle);
|
||||||
|
|
||||||
|
// 内外半径
|
||||||
|
const innerR = r - strokeWidth / 2;
|
||||||
|
const outerR = r + strokeWidth / 2;
|
||||||
|
|
||||||
|
// 内外圆弧的起点和终点
|
||||||
|
const innerStartX = cx + innerR * Math.cos(startAngle);
|
||||||
|
const innerStartY = cy + innerR * Math.sin(startAngle);
|
||||||
|
const innerEndX = cx + innerR * Math.cos(endAngle);
|
||||||
|
const innerEndY = cy + innerR * Math.sin(endAngle);
|
||||||
|
|
||||||
|
const outerStartX = cx + outerR * Math.cos(startAngle);
|
||||||
|
const outerStartY = cy + outerR * Math.sin(startAngle);
|
||||||
|
const outerEndX = cx + outerR * Math.cos(endAngle);
|
||||||
|
const outerEndY = cy + outerR * Math.sin(endAngle);
|
||||||
|
|
||||||
|
// 创建路径
|
||||||
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
|
|
||||||
|
// 大弧标志 (large-arc-flag)
|
||||||
|
const largeArcFlag = segmentPercent > 0.5 ? 1 : 0;
|
||||||
|
|
||||||
|
// 路径数据
|
||||||
|
const d = [
|
||||||
|
`M ${innerEndX} ${innerEndY}`,
|
||||||
|
`A ${innerR} ${innerR} 0 ${largeArcFlag} 0 ${innerStartX} ${innerStartY}`,
|
||||||
|
`L ${outerStartX} ${outerStartY}`,
|
||||||
|
`A ${outerR} ${outerR} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY}`,
|
||||||
|
'Z'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
path.setAttribute('d', d);
|
||||||
|
path.setAttribute('fill', seg.color);
|
||||||
|
|
||||||
|
// 添加过渡动画
|
||||||
|
path.style.transition = 'all 0.5s ease-out';
|
||||||
|
|
||||||
|
// 添加高亮效果
|
||||||
|
path.addEventListener('mouseenter', () => {
|
||||||
|
path.setAttribute('opacity', '0.8');
|
||||||
|
});
|
||||||
|
path.addEventListener('mouseleave', () => {
|
||||||
|
path.setAttribute('opacity', '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
svgElement.appendChild(path);
|
||||||
|
|
||||||
|
// 更新起始角度
|
||||||
|
startAngle = endAngle;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图例显示归一化后的分段
|
||||||
|
if (legendElement) {
|
||||||
|
legendElement.innerHTML = '';
|
||||||
|
normalizedSegments.forEach(seg => {
|
||||||
|
renderLegendItem(legendElement, seg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染图例项
|
||||||
|
function renderLegendItem(legendElement, segment, overridePercent) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'pie-legend-item';
|
||||||
|
|
||||||
|
const colorSpan = document.createElement('span');
|
||||||
|
colorSpan.className = 'pie-legend-color';
|
||||||
|
colorSpan.style.backgroundColor = segment.color;
|
||||||
|
|
||||||
|
const labelSpan = document.createElement('span');
|
||||||
|
labelSpan.className = 'pie-legend-label';
|
||||||
|
labelSpan.textContent = segment.label;
|
||||||
|
|
||||||
|
const rangeSpan = document.createElement('span');
|
||||||
|
rangeSpan.className = 'pie-legend-range';
|
||||||
|
rangeSpan.textContent = segment.range;
|
||||||
|
|
||||||
|
const valueSpan = document.createElement('span');
|
||||||
|
valueSpan.className = 'pie-legend-value';
|
||||||
|
valueSpan.textContent = `${Math.round(overridePercent || segment.percent)}%`;
|
||||||
|
|
||||||
|
item.appendChild(colorSpan);
|
||||||
|
item.appendChild(labelSpan);
|
||||||
|
item.appendChild(rangeSpan);
|
||||||
|
item.appendChild(valueSpan);
|
||||||
|
|
||||||
|
legendElement.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听父窗口传来的数据
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
showLoading();
|
||||||
|
if (event.data?.type === 'INIT_SPO2_REPORT') {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
// 保存全局数据用于饼图中心显示
|
||||||
|
window.reportData = event.data.data;
|
||||||
|
fillData(event.data.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('填充报告数据失败:', error);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 支持独立打开时自动填充测试数据
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
showLoading();
|
||||||
|
if (!window.opener && !window.parent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fillData({
|
||||||
|
patientInfo: { name: '测试患者', gender: '1', age: 45, examid: 'SPO2-2024-001', weartime: '2024-01-20', devicename: 'SPO2-Monitor-X1' },
|
||||||
|
spo2Stats: { averageSpO2: 96, minSpO2: 88, maxSpO2: 99, lowOxygenTime: 15 },
|
||||||
|
timePeriodData: {
|
||||||
|
all: { excellent: { value: 360, percentage: 75 }, good: { value: 96, percentage: 20 }, warning: { value: 24, percentage: 5 }, danger: { value: 0, percentage: 0 } },
|
||||||
|
daytime: { excellent: { value: 240, percentage: 80 }, good: { value: 54, percentage: 18 }, warning: { value: 6, percentage: 2 }, danger: { value: 0, percentage: 0 } },
|
||||||
|
nighttime: { excellent: { value: 120, percentage: 67 }, good: { value: 42, percentage: 23 }, warning: { value: 18, percentage: 10 }, danger: { value: 0, percentage: 0 } }
|
||||||
|
},
|
||||||
|
dataTableList: [
|
||||||
|
{ spo2value: 88, measuretime: '2024-01-20 02:00:00' },
|
||||||
|
{ spo2value: 99, measuretime: '2024-01-20 14:00:00' }
|
||||||
|
],
|
||||||
|
diagnosisForm: { conclusion: '患者血氧饱和度总体表现良好,夜间偶有轻微低氧情况,建议继续观察。' }
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -17,6 +17,7 @@ export interface Spo2infoVO {
|
|||||||
superiorrequest: number // 上级请求
|
superiorrequest: number // 上级请求
|
||||||
createtime: Date // 创建时间
|
createtime: Date // 创建时间
|
||||||
updatetime: Date // 更新时间
|
updatetime: Date // 更新时间
|
||||||
|
diagnosis: string // 诊断结论
|
||||||
}
|
}
|
||||||
|
|
||||||
// 血氧信息 API
|
// 血氧信息 API
|
||||||
@ -77,6 +78,14 @@ export const Spo2infoApi = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//更新诊断结果
|
||||||
|
upSpoInfoDiagnosis: async (data: any) => {
|
||||||
|
return await request.put({
|
||||||
|
url: `/system/spo2info/update-diagnosis`,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// 获取血氧信息统计数据
|
// 获取血氧信息统计数据
|
||||||
getSpO2Analysis: async (regid: string, examid: string, weartime: string) => {
|
getSpO2Analysis: async (regid: string, examid: string, weartime: string) => {
|
||||||
return await request.get({
|
return await request.get({
|
||||||
|
|||||||
@ -225,6 +225,33 @@
|
|||||||
|
|
||||||
<!-- 数据统计内容 -->
|
<!-- 数据统计内容 -->
|
||||||
<div v-if="activeTab === 'data-stats'" class="tab-content">
|
<div v-if="activeTab === 'data-stats'" class="tab-content">
|
||||||
|
<!-- 等级分类说明 -->
|
||||||
|
<div class="level-classification-info">
|
||||||
|
<h4>血氧等级分类说明</h4>
|
||||||
|
<div class="level-grid">
|
||||||
|
<div class="level-item">
|
||||||
|
<el-tag type="success" size="small">优秀</el-tag>
|
||||||
|
<span class="level-range">≥95%</span>
|
||||||
|
<span class="level-desc">血氧饱和度正常,状态良好</span>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<el-tag type="primary" size="small">良好</el-tag>
|
||||||
|
<span class="level-range">90-94%</span>
|
||||||
|
<span class="level-desc">血氧饱和度轻度偏低,需要关注</span>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<el-tag type="warning" size="small">偏低</el-tag>
|
||||||
|
<span class="level-range">85-89%</span>
|
||||||
|
<span class="level-desc">血氧饱和度明显偏低,需要干预</span>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<el-tag type="danger" size="small">危险</el-tag>
|
||||||
|
<span class="level-range"><85%</span>
|
||||||
|
<span class="level-desc">血氧饱和度严重不足,需要紧急处理</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="data-table-container">
|
<div class="data-table-container">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<h4>监测数据列表</h4>
|
<h4>监测数据列表</h4>
|
||||||
@ -253,9 +280,14 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="spo2value" label="血氧饱和度" min-width="120" align="center">
|
<el-table-column prop="spo2value" label="血氧饱和度" min-width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="spo2-value" :class="getSpO2Class(row.spo2value)">
|
<span class="spo2-value"> {{ row.spo2value }}% </span>
|
||||||
{{ row.spo2value }}%
|
</template>
|
||||||
</span>
|
</el-table-column>
|
||||||
|
<el-table-column prop="spo2Level" label="血氧等级" min-width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getSpO2TagType(row.spo2value)" size="small">
|
||||||
|
{{ getSpO2LevelText(row.spo2value) }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="pulsevalue" label="脉率" min-width="100" align="center">
|
<el-table-column prop="pulsevalue" label="脉率" min-width="100" align="center">
|
||||||
@ -351,7 +383,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: boolean]
|
'update:modelValue': [value: boolean]
|
||||||
save: [data: any]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@ -365,13 +396,15 @@ const saving = ref(false)
|
|||||||
|
|
||||||
// 患者信息
|
// 患者信息
|
||||||
const patientInfo = ref({
|
const patientInfo = ref({
|
||||||
|
id: 0, // 患者记录ID
|
||||||
name: '',
|
name: '',
|
||||||
gender: '',
|
gender: '',
|
||||||
age: '',
|
age: '',
|
||||||
regid: '',
|
regid: '',
|
||||||
examid: '',
|
examid: '',
|
||||||
weartime: '',
|
weartime: '',
|
||||||
devicename: ''
|
devicename: '',
|
||||||
|
diagnosis: '' // 新增诊断结论字段
|
||||||
})
|
})
|
||||||
|
|
||||||
// 血氧统计数据
|
// 血氧统计数据
|
||||||
@ -499,21 +532,19 @@ const handleSave = async () => {
|
|||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
// 调用更新诊断结果接口
|
||||||
|
const updateData = {
|
||||||
const saveData = {
|
id: patientInfo.value.id, // 患者记录ID
|
||||||
patientId: patientInfo.value.examid,
|
diagnosis: diagnosisForm.conclusion // 诊断结论
|
||||||
diagnosis: diagnosisForm,
|
|
||||||
stats: {
|
|
||||||
spo2: spo2Stats.value,
|
|
||||||
data: dataStats.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('save', saveData)
|
console.log('更新诊断结果参数:', updateData)
|
||||||
ElMessage.success('保存成功')
|
await Spo2infoApi.upSpoInfoDiagnosis(updateData)
|
||||||
|
|
||||||
|
ElMessage.success('诊断结果保存成功')
|
||||||
visible.value = false
|
visible.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('保存诊断结果失败:', error)
|
||||||
ElMessage.error('保存失败')
|
ElMessage.error('保存失败')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@ -541,6 +572,22 @@ const getSpO2Class = (value: number) => {
|
|||||||
return 'spo2-danger'
|
return 'spo2-danger'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取血氧等级文本
|
||||||
|
const getSpO2LevelText = (value: number) => {
|
||||||
|
if (value >= 95) return '优秀'
|
||||||
|
if (value >= 90) return '良好'
|
||||||
|
if (value >= 85) return '偏低'
|
||||||
|
return '危险'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取血氧等级标签类型
|
||||||
|
const getSpO2TagType = (value: number) => {
|
||||||
|
if (value >= 95) return 'success'
|
||||||
|
if (value >= 90) return 'primary'
|
||||||
|
if (value >= 85) return 'warning'
|
||||||
|
return 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
// 获取数据表格数据
|
// 获取数据表格数据
|
||||||
const getDataTableList = async () => {
|
const getDataTableList = async () => {
|
||||||
// 确保有患者数据
|
// 确保有患者数据
|
||||||
@ -739,6 +786,43 @@ const getAnalysisData = async () => {
|
|||||||
|
|
||||||
// 重置时间段选择
|
// 重置时间段选择
|
||||||
selectedTimePeriod.value = 'all'
|
selectedTimePeriod.value = 'all'
|
||||||
|
|
||||||
|
// 自动生成诊断结论
|
||||||
|
const generateDiagnosisText = (stats, timePeriodData) => {
|
||||||
|
// 计算分析建议
|
||||||
|
const getSimpleAnalysis = () => {
|
||||||
|
const day = timePeriodData.daytime
|
||||||
|
const night = timePeriodData.nighttime
|
||||||
|
let result: string[] = []
|
||||||
|
if (day.warning.percentage > 10 || day.danger.percentage > 0) {
|
||||||
|
result.push('白天偏低或危险占比高,建议关注白天血氧变化。')
|
||||||
|
} else {
|
||||||
|
result.push('白天血氧整体良好。')
|
||||||
|
}
|
||||||
|
if (night.warning.percentage > 10 || night.danger.percentage > 0) {
|
||||||
|
result.push('夜间偏低或危险占比高,建议夜间加强监测。')
|
||||||
|
} else {
|
||||||
|
result.push('夜间血氧整体良好。')
|
||||||
|
}
|
||||||
|
return result.join(' ')
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`全程共测量${stats.totalRecords || '--'}次,有效率100%;`,
|
||||||
|
`平均血氧饱和度:${stats.averageSpO2 || '--'}%;最低血氧饱和度:${stats.minSpO2 || '--'}%;最高血氧饱和度:${stats.maxSpO2 || '--'}%;低氧时间:${stats.lowOxygenTime > 0 ? stats.lowOxygenTime + '分钟' : '无低氧'};`,
|
||||||
|
`全天分布:优秀${timePeriodData.all.excellent.value || 0}次(${timePeriodData.all.excellent.percentage || 0}% ),良好${timePeriodData.all.good.value || 0}次(${timePeriodData.all.good.percentage || 0}% ),偏低${timePeriodData.all.warning.value || 0}次(${timePeriodData.all.warning.percentage || 0}% ),危险${timePeriodData.all.danger.value || 0}次(${timePeriodData.all.danger.percentage || 0}% );`,
|
||||||
|
`白天分布:优秀${timePeriodData.daytime.excellent.value || 0}次(${timePeriodData.daytime.excellent.percentage || 0}% ),良好${timePeriodData.daytime.good.value || 0}次(${timePeriodData.daytime.good.percentage || 0}% ),偏低${timePeriodData.daytime.warning.value || 0}次(${timePeriodData.daytime.warning.percentage || 0}% ),危险${timePeriodData.daytime.danger.value || 0}次(${timePeriodData.daytime.danger.percentage || 0}% );`,
|
||||||
|
`夜间分布:优秀${timePeriodData.nighttime.excellent.value || 0}次(${timePeriodData.nighttime.excellent.percentage || 0}% ),良好${timePeriodData.nighttime.good.value || 0}次(${timePeriodData.nighttime.good.percentage || 0}% ),偏低${timePeriodData.nighttime.warning.value || 0}次(${timePeriodData.nighttime.warning.percentage || 0}% ),危险${timePeriodData.nighttime.danger.value || 0}次(${timePeriodData.nighttime.danger.percentage || 0}% );`,
|
||||||
|
`分析:${getSimpleAnalysis()}`,
|
||||||
|
`建议:请结合患者实际情况综合分析。`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
// 如果诊断结论为空,自动填充
|
||||||
|
if (!diagnosisForm.conclusion) {
|
||||||
|
diagnosisForm.conclusion = generateDiagnosisText(
|
||||||
|
{ ...spo2Stats.value, totalRecords: response.totalRecords },
|
||||||
|
timePeriodData.value
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取血氧分析数据失败:', error)
|
console.error('获取血氧分析数据失败:', error)
|
||||||
ElMessage.error('获取分析数据失败')
|
ElMessage.error('获取分析数据失败')
|
||||||
@ -751,6 +835,12 @@ const getAnalysisData = async () => {
|
|||||||
const updatePatientInfo = () => {
|
const updatePatientInfo = () => {
|
||||||
if (props.patientData) {
|
if (props.patientData) {
|
||||||
patientInfo.value = { ...props.patientData }
|
patientInfo.value = { ...props.patientData }
|
||||||
|
// 优先加载表里的诊断结论
|
||||||
|
if (patientInfo.value.diagnosis) {
|
||||||
|
diagnosisForm.conclusion = patientInfo.value.diagnosis
|
||||||
|
} else {
|
||||||
|
diagnosisForm.conclusion = ''
|
||||||
|
}
|
||||||
// 患者信息更新后,自动获取分析数据
|
// 患者信息更新后,自动获取分析数据
|
||||||
getAnalysisData()
|
getAnalysisData()
|
||||||
}
|
}
|
||||||
@ -1281,6 +1371,51 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等级分类说明样式
|
||||||
|
.level-classification-info {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.level-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
|
||||||
|
.level-range {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 数据表格样式
|
// 数据表格样式
|
||||||
.data-table-container {
|
.data-table-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -1344,29 +1479,8 @@ defineExpose({
|
|||||||
|
|
||||||
.spo2-value {
|
.spo2-value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
&.spo2-excellent {
|
|
||||||
background: #f0f9ff;
|
|
||||||
color: #0ea5e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.spo2-good {
|
|
||||||
background: #f0fdf4;
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.spo2-warning {
|
|
||||||
background: #fefce8;
|
|
||||||
color: #ca8a04;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.spo2-danger {
|
|
||||||
background: #fef2f2;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pulse-value {
|
.pulse-value {
|
||||||
|
|||||||
@ -290,6 +290,26 @@
|
|||||||
:patient-data="currentPatient"
|
:patient-data="currentPatient"
|
||||||
@save="handleAnalysisSave"
|
@save="handleAnalysisSave"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 报告模版弹窗(iframe方式) -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="iframeDialogVisible"
|
||||||
|
title="血氧监测报告"
|
||||||
|
fullscreen
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
ref="reportIframe"
|
||||||
|
:src="iframeReportUrl"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
frameborder="0"
|
||||||
|
style="min-height: 100vh"
|
||||||
|
@load="onIframeLoad"
|
||||||
|
></iframe>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -303,6 +323,8 @@ import PatientSelect from '@/patientcom/index.vue'
|
|||||||
import { Icon } from '@/components/Icon'
|
import { Icon } from '@/components/Icon'
|
||||||
import { Search, Refresh, Plus, Download } from '@element-plus/icons-vue'
|
import { Search, Refresh, Plus, Download } from '@element-plus/icons-vue'
|
||||||
import { OrgApi } from '@/api/org'
|
import { OrgApi } from '@/api/org'
|
||||||
|
// 新增:引入报告模版组件
|
||||||
|
import ReportTemplate from '@/views/analysis/Spo2/ReportTemplate.vue'
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@ -315,6 +337,16 @@ const Profilevo = ref<any>({})
|
|||||||
const analysisDialogVisible = ref(false)
|
const analysisDialogVisible = ref(false)
|
||||||
const currentPatient = ref<any>({})
|
const currentPatient = ref<any>({})
|
||||||
|
|
||||||
|
// 报告弹窗相关
|
||||||
|
const reportDialogVisible = ref(false)
|
||||||
|
const reportData = ref<any>({})
|
||||||
|
|
||||||
|
// 新增:iframe弹窗相关
|
||||||
|
const iframeDialogVisible = ref(false)
|
||||||
|
const iframeReportUrl = ref('')
|
||||||
|
const reportIframe = ref()
|
||||||
|
let lastReportData: any = null
|
||||||
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -420,9 +452,132 @@ const handleAnalysisSave = (data: any) => {
|
|||||||
getList() // 刷新列表
|
getList() // 刷新列表
|
||||||
}
|
}
|
||||||
// 查看
|
// 查看
|
||||||
const handleView = (row: any) => {
|
const handleView = async (row: any) => {
|
||||||
// TODO: 调用查看接口
|
if (!row.diagnosis || row.diagnosis.trim() === '') {
|
||||||
ElMessage.info('查看功能待实现')
|
ElMessage.warning('请先填写诊断结论后再查看报告')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 格式化 weartime
|
||||||
|
const formatDateTimeForAPI = (dateTime: string | Date) => {
|
||||||
|
if (!dateTime) return ''
|
||||||
|
const d = new Date(dateTime)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 1. 查分析数据
|
||||||
|
const weartimeStr = formatDateTimeForAPI(row.weartime)
|
||||||
|
const analysis = await Spo2infoApi.getSpO2Analysis(row.regid, row.examid, weartimeStr)
|
||||||
|
// 2. 查明细列表
|
||||||
|
const dataTableRes = await (
|
||||||
|
await import('@/api/spo2data')
|
||||||
|
).Spo2dataApi.getSpo2dataPage({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
regid: row.regid,
|
||||||
|
examid: row.examid,
|
||||||
|
weartime: weartimeStr
|
||||||
|
})
|
||||||
|
// 3. 百分比算法
|
||||||
|
const calcPercent = (count: number, total: number) =>
|
||||||
|
total > 0 ? Math.round((count / total) * 100) : 0
|
||||||
|
const totalRecords = analysis.totalRecords || 0
|
||||||
|
const timePeriodData = {
|
||||||
|
all: {
|
||||||
|
excellent: {
|
||||||
|
value: analysis.excellentCount || 0,
|
||||||
|
percentage: calcPercent(analysis.excellentCount || 0, totalRecords)
|
||||||
|
},
|
||||||
|
good: {
|
||||||
|
value: analysis.goodCount || 0,
|
||||||
|
percentage: calcPercent(analysis.goodCount || 0, totalRecords)
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
value: analysis.warningCount || 0,
|
||||||
|
percentage: calcPercent(analysis.warningCount || 0, totalRecords)
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
value: analysis.dangerCount || 0,
|
||||||
|
percentage: calcPercent(analysis.dangerCount || 0, totalRecords)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
daytime: (() => {
|
||||||
|
const total =
|
||||||
|
(analysis.daytimeExcellentCount || 0) +
|
||||||
|
(analysis.daytimeGoodCount || 0) +
|
||||||
|
(analysis.daytimeWarningCount || 0) +
|
||||||
|
(analysis.daytimeDangerCount || 0)
|
||||||
|
return {
|
||||||
|
excellent: {
|
||||||
|
value: analysis.daytimeExcellentCount || 0,
|
||||||
|
percentage: calcPercent(analysis.daytimeExcellentCount || 0, total)
|
||||||
|
},
|
||||||
|
good: {
|
||||||
|
value: analysis.daytimeGoodCount || 0,
|
||||||
|
percentage: calcPercent(analysis.daytimeGoodCount || 0, total)
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
value: analysis.daytimeWarningCount || 0,
|
||||||
|
percentage: calcPercent(analysis.daytimeWarningCount || 0, total)
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
value: analysis.daytimeDangerCount || 0,
|
||||||
|
percentage: calcPercent(analysis.daytimeDangerCount || 0, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
nighttime: (() => {
|
||||||
|
const total =
|
||||||
|
(analysis.nighttimeExcellentCount || 0) +
|
||||||
|
(analysis.nighttimeGoodCount || 0) +
|
||||||
|
(analysis.nighttimeWarningCount || 0) +
|
||||||
|
(analysis.nighttimeDangerCount || 0)
|
||||||
|
return {
|
||||||
|
excellent: {
|
||||||
|
value: analysis.nighttimeExcellentCount || 0,
|
||||||
|
percentage: calcPercent(analysis.nighttimeExcellentCount || 0, total)
|
||||||
|
},
|
||||||
|
good: {
|
||||||
|
value: analysis.nighttimeGoodCount || 0,
|
||||||
|
percentage: calcPercent(analysis.nighttimeGoodCount || 0, total)
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
value: analysis.nighttimeWarningCount || 0,
|
||||||
|
percentage: calcPercent(analysis.nighttimeWarningCount || 0, total)
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
value: analysis.nighttimeDangerCount || 0,
|
||||||
|
percentage: calcPercent(analysis.nighttimeDangerCount || 0, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
// 4. 组装数据
|
||||||
|
const reportData = {
|
||||||
|
patientInfo: row,
|
||||||
|
spo2Stats: {
|
||||||
|
averageSpO2: analysis.averageSpO2,
|
||||||
|
minSpO2: analysis.minSpO2,
|
||||||
|
maxSpO2: analysis.maxSpO2,
|
||||||
|
lowOxygenTime: analysis.lowOxygenTime
|
||||||
|
},
|
||||||
|
timePeriodData,
|
||||||
|
dataTableList: dataTableRes.list || [],
|
||||||
|
diagnosisForm: { conclusion: row.diagnosis }
|
||||||
|
}
|
||||||
|
// 新增:弹窗展示iframe
|
||||||
|
iframeReportUrl.value = '/spo2-report-template.html'
|
||||||
|
iframeDialogVisible.value = true
|
||||||
|
lastReportData = reportData
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误详情:', err) // 调试日志
|
||||||
|
ElMessage.error('获取报告数据失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 申请
|
// 申请
|
||||||
const handleApply = async (row: any) => {
|
const handleApply = async (row: any) => {
|
||||||
@ -520,9 +675,35 @@ const cancelDeviceChange = (row: any) => {
|
|||||||
row.tempDeviceId = null
|
row.tempDeviceId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增:iframe加载后发送数据
|
||||||
|
defineExpose({ reportIframe })
|
||||||
|
const onIframeLoad = () => {
|
||||||
|
if (reportIframe.value && lastReportData) {
|
||||||
|
// 深拷贝,保证postMessage数据可被结构化克隆
|
||||||
|
const safeData = JSON.parse(JSON.stringify(lastReportData))
|
||||||
|
reportIframe.value.contentWindow.postMessage({ type: 'INIT_SPO2_REPORT', data: safeData }, '*')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
Profilevo.value = await getUserProfile()
|
Profilevo.value = await getUserProfile()
|
||||||
getList()
|
getList()
|
||||||
|
// 新增:监听iframe请求数据
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (
|
||||||
|
event.data?.type === 'REQUEST_SPO2_REPORT' &&
|
||||||
|
iframeDialogVisible.value &&
|
||||||
|
lastReportData &&
|
||||||
|
reportIframe.value
|
||||||
|
) {
|
||||||
|
// 深拷贝,保证postMessage数据可被结构化克隆
|
||||||
|
const safeData = JSON.parse(JSON.stringify(lastReportData))
|
||||||
|
reportIframe.value.contentWindow.postMessage(
|
||||||
|
{ type: 'INIT_SPO2_REPORT', data: safeData },
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user