From bcfed5c9c8edbc3554e987be1ba11140aafa2ecd Mon Sep 17 00:00:00 2001 From: Flow <958079825@qq.com> Date: Thu, 17 Jul 2025 16:57:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=9B=BE=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/abpm-report-template.html | 363 ++++++++++++++++++++++++--- src/views/analysis/ABPM/analysis.vue | 271 +++++++++++++++----- 2 files changed, 537 insertions(+), 97 deletions(-) diff --git a/public/abpm-report-template.html b/public/abpm-report-template.html index 39b4cc0..d1d1900 100644 --- a/public/abpm-report-template.html +++ b/public/abpm-report-template.html @@ -204,7 +204,7 @@ .scatter-chart-item .chart { width: 100%; - height: 240px; + height: 320px; border: 1px solid #000; } @@ -219,7 +219,7 @@ } .data-table-container { - max-height: calc(100vh - 200px); + max-height: 1000px; overflow-y: auto; margin: 20px 0; height: auto; @@ -234,8 +234,9 @@ .data-table th, .data-table td { border: 1px solid #000; - padding: 5px; + padding: 3px; text-align: center; + line-height: 1.2; } .data-table th { @@ -1639,52 +1640,290 @@ function renderTrendChart() { const chart = echarts.init(document.getElementById('trend-chart')); - const timeLabels = chartDataTable.map(item => - new Date(item.originalTime).toLocaleDateString('zh-CN', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - ); + // 完全参考analysis.vue的实现,使用value类型的X轴显示精确时间位置 + const createBloodPressureXAxis = () => { + if (chartDataTable.length === 0) return { xAxisTicks: [], xAxisLabels: [], seriesMap: {} }; + + // 1. 取所有时间戳 + const times = chartDataTable.map(item => new Date(item.originalTime).getTime()); + const minTime = Math.min(...times); + const maxTime = Math.max(...times); + + // 2. 计算起止整点 + const start = new Date(minTime); + start.setMinutes(0, 0, 0); + const end = new Date(maxTime); + if (end.getMinutes() > 0 || end.getSeconds() > 0) { + end.setHours(end.getHours() + 1); + } + end.setMinutes(0, 0, 0); + + // 3. 生成刻度(整点+半点) + const xAxisTicks = []; + const xAxisLabels = []; + let cur = new Date(start); + while (cur <= end) { + xAxisTicks.push(cur.getTime()); + xAxisLabels.push(formatTimeLabel(cur)); + // 半点 + const half = new Date(cur); + half.setMinutes(30); + if (half < end) { + xAxisTicks.push(half.getTime()); + xAxisLabels.push(''); // 半点不显示label + } + cur.setHours(cur.getHours() + 1); + cur.setMinutes(0); + } + + // 4. 生成series数据,每个数据点都有精确的时间戳位置 + const getSeriesData = (key) => { + return chartDataTable.map(item => ({ + value: [new Date(item.originalTime).getTime(), item[key] || null], + itemTime: item.originalTime + })); + }; + + return { + xAxisTicks, + xAxisLabels, + seriesMap: { + systolic: getSeriesData('systolic'), + diastolic: getSeriesData('diastolic'), + heartRate: getSeriesData('heartRate') + } + }; + }; + const formatTimeLabel = (date) => { + const day = date.getDate(); + const hour = date.getHours(); + return `${hour.toString().padStart(2, '0')}`; + }; + + const { xAxisTicks, xAxisLabels, seriesMap } = createBloodPressureXAxis(); + + // 计算夜间遮罩区域(每一天的22点到次日8点),使用时间戳方式 + const calculateNightAreas = () => { + const nightAreas = []; + const startTime = new Date(Math.min(...chartDataTable.map(item => new Date(item.originalTime).getTime()))); + const endTime = new Date(Math.max(...chartDataTable.map(item => new Date(item.originalTime).getTime()))); + + // 从第一天开始循环,为每一天生成夜间区域 + let currentDate = new Date(startTime); + currentDate.setHours(0, 0, 0, 0); // 设置为当天0点 + + while (currentDate <= endTime) { + // 当天22点 + const nightStart = new Date(currentDate); + nightStart.setHours(22, 0, 0, 0); + + // 次日8点 + const nightEnd = new Date(currentDate); + nightEnd.setDate(nightEnd.getDate() + 1); + nightEnd.setHours(8, 0, 0, 0); + + // 只有当夜间时段与数据时间范围有交集时才添加 + if (nightStart <= endTime && nightEnd >= startTime) { + nightAreas.push([ + { xAxis: Math.max(nightStart.getTime(), startTime.getTime()) }, + { xAxis: Math.min(nightEnd.getTime(), endTime.getTime()) } + ]); + } + + // 移动到下一天 + currentDate.setDate(currentDate.getDate() + 1); + } + + return nightAreas; + }; + const nightAreas = calculateNightAreas(); + + // 完全参考analysis.vue的图表配置,使用value类型X轴 const option = { - tooltip: { trigger: 'axis' }, - legend: { data: ['收缩压', '舒张压', '心率'], top: 10 }, - xAxis: { type: 'category', data: timeLabels }, + grid: { + left: 50, + right: 50, + bottom: 60, + top: 50, + containLabel: true + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross' + }, + backgroundColor: 'rgba(50,50,50,0.7)', + borderWidth: 0, + textStyle: { + color: '#fff' + }, + formatter: function(params) { + if (params && params.length > 0) { + let html = ''; + params.forEach((param) => { + if (param.data && param.data.itemTime) { + const time = new Date(param.data.itemTime); + const timeStr = time.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + html += `${param.seriesName}: ${param.data.value[1] || '--'}
`; + if (param.seriesIndex === 0) { + html += `时间: ${timeStr}
`; + } + } + }); + return html; + } + return ''; + } + }, + legend: { + data: ['收缩压', '舒张压', '心率'], + top: 10, + left: 'center' + }, + xAxis: { + type: 'value', + min: xAxisTicks[0], + max: xAxisTicks[xAxisTicks.length - 1], + interval: (60 * 60 * 1000) / 2, // 半小时 + minInterval: (60 * 60 * 1000) / 2, // 半小时 + axisLabel: { + formatter: (value) => { + const date = new Date(value); + // 只在整点显示 + if (date.getMinutes() === 0) { + return `${date.getHours().toString().padStart(2, '0')}`; + } + return ''; + }, + rotate: 0, + color: '#666' + }, + axisLine: { + lineStyle: { + color: '#e0e0e0' + } + }, + axisTick: { + show: false + }, + splitLine: { + show: false, + lineStyle: { + color: '#e0e0e0', + type: 'solid' + } + } + }, yAxis: [ - { type: 'value', name: 'mmHg', position: 'left', min: 0, max: 200 }, - { type: 'value', name: '次/分', position: 'right', min: 50, max: 120 } + { + type: 'value', + name: 'mmHg', + position: 'left', + min: 0, + max: 200, + interval: 20, + axisLabel: { + color: '#666' + }, + axisLine: { + lineStyle: { + color: '#e0e0e0' + } + }, + splitLine: { + show: true, + lineStyle: { + color: '#f0f0f0', + type: 'dashed' + } + } + }, + { + type: 'value', + name: '次/分', + position: 'right', + min: 50, + max: 120, + interval: 10, + axisLabel: { + color: '#666' + }, + axisLine: { + lineStyle: { + color: '#e0e0e0' + } + }, + splitLine: { + show: false + } + } ], series: [ { name: '收缩压', type: 'line', - symbol: 'none', + symbol: 'circle', + symbolSize: 6, lineStyle: { width: 2 }, - data: chartDataTable.map(item => item.systolic), - itemStyle: { color: '#409eff' } + connectNulls: false, + smooth: false, + data: seriesMap.systolic, + itemStyle: { + color: '#409eff', + borderWidth: 1, + borderColor: '#fff' + }, + markArea: { + silent: true, + itemStyle: { + color: 'rgba(128, 128, 128, 0.2)', + borderWidth: 0 + }, + data: nightAreas, + label: { + show: false + } + } }, { name: '舒张压', type: 'line', - symbol: 'none', + symbol: 'circle', + symbolSize: 6, lineStyle: { width: 2 }, - data: chartDataTable.map(item => item.diastolic), - itemStyle: { color: '#67c23a' } + connectNulls: false, + smooth: false, + data: seriesMap.diastolic, + itemStyle: { + color: '#67c23a', + borderWidth: 1, + borderColor: '#fff' + } }, { name: '心率', type: 'line', - symbol: 'none', + symbol: 'circle', + symbolSize: 6, lineStyle: { width: 2 }, + connectNulls: false, + smooth: false, yAxisIndex: 1, - data: chartDataTable.map(item => item.heartRate), - itemStyle: { color: '#e6a23c' } + data: seriesMap.heartRate, + itemStyle: { + color: '#e6a23c', + borderWidth: 1, + borderColor: '#fff' + } } ] }; - chart.setOption(option); } @@ -1826,31 +2065,59 @@ let normalCount, abnormalCount; if (field === 'heartRate') { - normalCount = data.filter(item => item[field] >= 60 && item[field] <= 100).length; + normalCount = data.filter(item => item[field] >=60 && item[field] <= 100).length; abnormalCount = data.length - normalCount; } else { normalCount = data.filter(item => item[field] < threshold).length; abnormalCount = data.length - normalCount; } - const normalPercent = data.length > 0 ? Math.round((normalCount / data.length) * 100) : 0; - const abnormalPercent = 100 - normalPercent; + // 构建数据数组,只包含有数据的类别 + const pieData = []; + + // 只有当正常数据存在时才添加 + if (normalCount > 0) pieData.push({ + value: normalCount, + name: '正常', + itemStyle: { color: '#7fc7ff' } + }); + + // 只有当异常数据存在时才添加 + if (abnormalCount > 0) pieData.push({ + value: abnormalCount, + name: '异常', + itemStyle: { color: '#ff7f7f' } + }); + + // 如果没有任何数据,显示提示 + if (pieData.length === 0) pieData.push({ + value: 1, + name: '暂无数据', + itemStyle: { color: '#f0f0f0' } + }); const option = { tooltip: { trigger: 'item', - formatter: '{b}: {c}次 ({d}%)' + formatter: function(params) { + if (params.name === '暂无数据') { + return '暂无数据'; + } + return params.name + ': ' + params.value + '次 (' + params.percent + '%)'; + } }, series: [{ type: 'pie', radius: '60%', - data: [ - { value: normalCount, itemStyle: { color: '#7fc7ff' } }, - { value: abnormalCount, itemStyle: { color: '#ff7f7f' } } - ], + data: pieData, label: { show: true, - formatter: '{b}\n{c}次\n{d}%', + formatter: function(params) { + if (params.name === '暂无数据') { + return '暂无数据'; + } + return params.name + '\n' + params.value + '次\n' + params.percent + '%'; + }, fontSize: 10, position: 'inside' }, @@ -2063,23 +2330,41 @@ name: '收缩压', type: 'line', smooth: true, + symbol: 'circle', + symbolSize: 6, // 增加点的大小 data: hourlyData.map(d => d.systolic), - itemStyle: { color: '#409eff' } + itemStyle: { + color: '#409eff', + borderWidth: 1, + borderColor: '#fff' // 添加白色边框使点更明显 + } }, { name: '舒张压', type: 'line', smooth: true, + symbol: 'circle', + symbolSize: 6, // 增加点的大小 data: hourlyData.map(d => d.diastolic), - itemStyle: { color: '#67c23a' } + itemStyle: { + color: '#67c23a', + borderWidth: 1, + borderColor: '#fff' // 添加白色边框使点更明显 + } }, { name: '心率', type: 'line', smooth: true, + symbol: 'circle', + symbolSize: 6, // 增加点的大小 yAxisIndex: 1, data: hourlyData.map(d => d.heartRate), - itemStyle: { color: '#e6a23c' } + itemStyle: { + color: '#e6a23c', + borderWidth: 1, + borderColor: '#fff' // 添加白色边框使点更明显 + } } ] }; @@ -2356,4 +2641,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/views/analysis/ABPM/analysis.vue b/src/views/analysis/ABPM/analysis.vue index 03bf7ed..9ffa44d 100644 --- a/src/views/analysis/ABPM/analysis.vue +++ b/src/views/analysis/ABPM/analysis.vue @@ -127,7 +127,7 @@
- + @@ -359,7 +357,72 @@ const updatePatientData = (rowData: any) => { patientData.orgname = rowData.orgname || '未知' } -// 处理血压数据的方法 +// 生成血压趋势图X轴和series数据 +function createBloodPressureXAxis( + data: { time: string; systolic: number; diastolic: number; heartRate?: number }[] +) { + // 1. 取所有时间戳 + const times = data.map((item) => new Date(item.time).getTime()) + if (times.length === 0) return { xAxisTicks: [], xAxisLabels: [], seriesMap: {} } + + // 2. 计算起止整点 + let minTime = Math.min(...times) + let maxTime = Math.max(...times) + + // 向下取整到小时 + const start = new Date(minTime) + start.setMinutes(0, 0, 0) + // 向上取整到下一个小时 + const end = new Date(maxTime) + if (end.getMinutes() > 0 || end.getSeconds() > 0) { + end.setHours(end.getHours() + 1) + } + end.setMinutes(0, 0, 0) + + // 3. 生成刻度(整点+半点) + const xAxisTicks: number[] = [] + const xAxisLabels: string[] = [] + let cur = new Date(start) + while (cur <= end) { + xAxisTicks.push(cur.getTime()) + xAxisLabels.push(formatTimeLabel(cur)) + // 半点 + const half = new Date(cur) + half.setMinutes(30) + if (half < end) { + xAxisTicks.push(half.getTime()) + xAxisLabels.push('') // 半点不显示label + } + cur.setHours(cur.getHours() + 1) + cur.setMinutes(0) + } + + // 4. 生成series数据 + function getSeriesData(key: 'systolic' | 'diastolic' | 'heartRate') { + return data.map((item) => ({ + value: [new Date(item.time).getTime(), item[key] ?? null], + itemTime: item.time + })) + } + + return { + xAxisTicks, + xAxisLabels, + seriesMap: { + systolic: getSeriesData('systolic'), + diastolic: getSeriesData('diastolic'), + heartRate: getSeriesData('heartRate') + } + } +} + +function formatTimeLabel(date: Date) { + const day = date.getDate() + const hour = date.getHours() + return `${day}日\n${hour.toString().padStart(2, '0')}:00` +} + +// 修改 processData,集成新的 X 轴和 series 生成逻辑 const processData = (rawList: any[], existingAnalysisResult?: string) => { // 先按测量时间排序 const sortedList = [...rawList].sort((a, b) => { @@ -368,26 +431,98 @@ const processData = (rawList: any[], existingAnalysisResult?: string) => { return timeA - timeB }) - // 1. 转换为图表数据格式 - const timeLabels = sortedList.map((item) => - dayjs(item.measuretime || item.weartime).format('DD日HH:mm') - ) - const systolicData = sortedList.map((item) => Number(item.systolicpressure) || 0) - const diastolicData = sortedList.map((item) => Number(item.diastolicpressure) || 0) - const heartRateData = sortedList.map((item) => Number(item.heartrate) || 0) + // 1. 转换为血压趋势图数据格式 + const bloodPressureData = sortedList.map((item) => ({ + time: dayjs(item.measuretime || item.weartime).format('YYYY-MM-DD HH:mm:ss'), + systolic: Number(item.systolicpressure) || 0, + diastolic: Number(item.diastolicpressure) || 0, + heartRate: Number(item.heartrate) || 0 + })) + const { xAxisTicks, xAxisLabels, seriesMap } = createBloodPressureXAxis(bloodPressureData) - const chartData = { - timeLabels, - systolicData, - diastolicData, - heartRateData + // 2. 更新图表配置(type: 'value',精确分布) + chartOption.value = { + ...chartOption.value, + xAxis: { + type: 'value', + min: xAxisTicks[0], + max: xAxisTicks[xAxisTicks.length - 1], + interval: (60 * 60 * 1000) / 2, // 半小时 + minInterval: (60 * 60 * 1000) / 2, // 半小时 + axisLabel: { + formatter: (value: number) => { + const date = new Date(value) + // 只在整点显示 + if (date.getMinutes() === 0) { + return `${date.getDate()}日\n${date.getHours().toString().padStart(2, '0')}:00` + } + return '' + }, + rotate: 0, + color: '#666' + }, + axisTick: { + show: false + }, + splitLine: { + show: false, // 不显示纵向线 + lineStyle: { + color: '#e0e0e0', + type: 'solid' + } + } + }, + series: [ + { + name: '收缩压统计', + type: 'line', + data: seriesMap.systolic, + smooth: false, + symbol: 'circle', + itemStyle: { color: '#409eff' }, + lineStyle: { width: 2 } + }, + { + name: '舒张压统计', + type: 'line', + data: seriesMap.diastolic, + smooth: false, + symbol: 'circle', + itemStyle: { color: '#67c23a' }, + lineStyle: { width: 2 } + }, + { + name: '心率统计', + type: 'line', + data: seriesMap.heartRate, + smooth: false, + symbol: 'circle', + itemStyle: { color: '#e6a23c' }, + lineStyle: { width: 2 } + } + ], + legend: { + data: ['收缩压统计', '舒张压统计', '心率统计'], + top: 10, + left: 'center' + }, + tooltip: { + trigger: 'axis', + formatter: function (params: any) { + let html = '' + params.forEach((param: any) => { + html += `${param.seriesName}: ${param.data.value[1]}
` + }) + html += `时间: ${params[0].data.itemTime}` + return html + } + } } - updateChartData(chartData) - // 2. 转换为表格数据格式 + // 3. 转换为表格数据格式 chartDataTable.value = sortedList.map((item) => ({ id: item.id, // 保留原始数据的id,用于删除操作 - time: dayjs(item.measuretime || item.weartime).format('DD日HH:mm'), + time: dayjs(item.measuretime || item.weartime).format('YYYY-MM-DD HH:mm:ss'), originalTime: item.measuretime || item.weartime, // 保存原始时间数据用于重新构建 systolic: Number(item.systolicpressure) || 0, diastolic: Number(item.diastolicpressure) || 0, @@ -396,16 +531,35 @@ const processData = (rawList: any[], existingAnalysisResult?: string) => { status: getBPStatus(Number(item.systolicpressure) || 0, Number(item.diastolicpressure) || 0) })) - // 3. 更新患者统计数据(从排序后的数据计算) + // 4. 更新患者统计数据(从排序后的数据计算) updatePatientStatistics(sortedList) - // 4. 设置分析结果:如果有现有的分析结果就使用,否则自动生成 + // 5. 设置分析结果:如果有现有的分析结果就使用,否则自动生成 if (existingAnalysisResult && existingAnalysisResult.trim()) { analysisResult.value = existingAnalysisResult } else { analysisResult.value = generateAnalysisReport(sortedList) } + // ...在 processData 里,数据处理后加上: + const allYValues = [ + ...sortedList.map((item) => Number(item.systolicpressure) || 0), + ...sortedList.map((item) => Number(item.diastolicpressure) || 0), + ...sortedList.map((item) => Number(item.heartrate) || 0) + ] + const maxY = Math.max(...allYValues) + const yAxisMax = Math.ceil(maxY / 10) * 10 + 10 + + chartOption.value = { + ...chartOption.value, + yAxis: { + ...chartOption.value.yAxis, + max: yAxisMax, + min: 0, + interval: 10 + } + } + // 数据更新完成后,显示图表 nextTick(() => { setTimeout(() => { @@ -848,30 +1002,21 @@ const chartOption = ref({ left: 'center' }, xAxis: { - type: 'category', - data: [], - axisTick: { - show: false - }, - axisLabel: { - color: '#666', - rotate: 45 - }, - axisLine: { - lineStyle: { - color: '#e0e0e0' - } - } - }, - yAxis: { type: 'value', min: 0, max: 260, - axisTick: { - show: false - }, + interval: (60 * 60 * 1000) / 2, // 半小时 + minInterval: (60 * 60 * 1000) / 2, // 半小时 axisLabel: { - color: '#666' + color: '#666', + rotate: 0, + formatter: (value: number) => { + const date = new Date(value) + const day = date.getDate() + const hour = date.getHours() + const minute = date.getMinutes() + return `${day}日\n${hour}:${minute.toString().padStart(2, '0')}` + } }, axisLine: { lineStyle: { @@ -879,6 +1024,32 @@ const chartOption = ref({ } }, splitLine: { + show: false, // 不显示纵向线 + lineStyle: { + color: '#e0e0e0', + type: 'solid' + } + } + }, + yAxis: { + type: 'value', + min: 0, + max: 160, + interval: 10, + axisTick: { + show: false + }, + axisLabel: { + color: '#666', + rotate: 0 + }, + axisLine: { + lineStyle: { + color: '#e0e0e0' + } + }, + splitLine: { + show: true, // 显示横向线 lineStyle: { color: '#f0f0f0', type: 'dashed' @@ -957,22 +1128,6 @@ const getHRColor = (value: number) => { return '#f56c6c' // 红色 - 过高 } -// 血压等级标签类型 -const getStatusType = (status: string) => { - switch (status) { - case '正常血压': - return 'success' - case '轻度高血压': - return 'warning' - case '中度高血压': - return 'danger' - case '重度高血压': - return 'danger' - default: - return 'info' - } -} - // 删除数据后重新计算和更新所有相关数据的方法 const refreshAllDataAfterDelete = () => { if (chartDataTable.value.length === 0) {