修改动态血氧功能模版

This commit is contained in:
lxd 2025-07-24 17:41:26 +08:00
parent dc83dbe497
commit 1cba193c9f
4 changed files with 872 additions and 41 deletions

View 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>危险 (&lt;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>

View File

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

View File

@ -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">&lt;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 {

View File

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