Fix Bug #566: AI修复

This commit is contained in:
2026-05-26 23:41:14 +08:00
parent 0f628d0ab6
commit 12dc9139ed
4 changed files with 216 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
package com.openhis.web.inpatient.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 住院体征数据 Mapper
* 修复 Bug #566确保查询结果按时间升序返回且字段名与前端映射一致
*/
@Mapper
public interface VitalSignsMapper {
@Select("SELECT id, patient_id, record_time, temperature, heart_rate, pulse, status " +
"FROM his_vital_signs " +
"WHERE patient_id = #{patientId} AND status = 1 " +
"ORDER BY record_time ASC")
List<Map<String, Object>> selectVitalSignsByPatient(@Param("patientId") Long patientId);
}

View File

@@ -0,0 +1,36 @@
package com.openhis.web.inpatient.service;
import com.openhis.web.inpatient.mapper.VitalSignsMapper;
import org.springframework.stereotype.Service;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 住院体征服务实现
* 修复 Bug #566统一时间格式为 yyyy-MM-dd HH:mm避免前端解析异常导致坐标映射失败
*/
@Service
public class VitalSignsServiceImpl implements VitalSignsService {
private final VitalSignsMapper vitalSignsMapper;
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
public VitalSignsServiceImpl(VitalSignsMapper vitalSignsMapper) {
this.vitalSignsMapper = vitalSignsMapper;
}
@Override
public List<Map<String, Object>> getVitalSignsData(Long patientId) {
List<Map<String, Object>> records = vitalSignsMapper.selectVitalSignsByPatient(patientId);
return records.stream().map(r -> {
// 统一时间格式供前端直接作为 xAxis 分类
if (r.get("record_time") != null) {
r.put("timeStr", r.get("record_time").toString().replace("T", " ").substring(0, 16));
r.put("recordTime", r.get("record_time"));
}
return r;
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,122 @@
<template>
<div class="vital-signs-container">
<div ref="chartRef" class="vital-signs-chart" style="height: 400px; width: 100%;"></div>
<el-table :data="tableData" class="vital-signs-table" border style="margin-top: 12px;">
<el-table-column prop="time" label="时间" width="160" align="center" />
<el-table-column prop="temperature" label="体温(℃)" align="center" />
<el-table-column prop="heartRate" label="心率(次/分)" align="center" />
<el-table-column prop="pulse" label="脉搏(次/分)" align="center" />
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'
import { getVitalSigns } from '@/api/inpatient'
const props = defineProps({
patientId: { type: Number, required: true }
})
const chartRef = ref(null)
let chartInstance = null
const tableData = ref([])
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
window.addEventListener('resize', handleResize)
}
const handleResize = () => chartInstance?.resize()
const renderChart = (rawData) => {
if (!chartInstance) return
// 1. 按时间升序排序
const sorted = [...rawData].sort((a, b) => new Date(a.recordTime) - new Date(b.recordTime))
const timeAxis = sorted.map(d => d.timeStr)
// 2. 提取指标序列,缺失值填 null 触发 ECharts 断线逻辑
const tempData = sorted.map(d => d.temperature != null ? Number(d.temperature) : null)
const hrData = sorted.map(d => d.heartRate != null ? Number(d.heartRate) : null)
const pulseData = sorted.map(d => d.pulse != null ? Number(d.pulse) : null)
// 3. 重叠处理同一时间点三值完全一致时Y轴轻微偏移避免视觉重合
const adjustOverlap = (series, type) => {
return series.map((val, idx) => {
if (val === null) return null
const t = tempData[idx], h = hrData[idx], p = pulseData[idx]
if (t !== null && h !== null && p !== null && t === h && h === p) {
return type === 'temp' ? val + 0.15 : type === 'hr' ? val + 0.08 : val
}
return val
})
}
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: timeAxis, axisLabel: { rotate: 30, interval: 0 } },
yAxis: [
{ type: 'value', name: '体温(℃)', min: 35, max: 42, splitNumber: 7, axisLine: { lineStyle: { color: '#1890ff' } } },
{ type: 'value', name: '心率/脉搏', min: 40, max: 160, splitNumber: 12, axisLine: { lineStyle: { color: '#f5222d' } } }
],
series: [
{
name: '体温', type: 'line', yAxisIndex: 0, data: adjustOverlap(tempData, 'temp'),
symbol: 'path://M0,0 L10,10 M10,0 L0,10', symbolSize: 10,
lineStyle: { color: '#1890ff', width: 2 }, connectNulls: false
},
{
name: '心率', type: 'line', yAxisIndex: 1, data: adjustOverlap(hrData, 'hr'),
symbol: 'circle', symbolSize: 10, itemStyle: { color: '#fff', borderColor: '#f5222d', borderWidth: 2 },
lineStyle: { color: '#f5222d', width: 2 }, connectNulls: false
},
{
name: '脉搏', type: 'line', yAxisIndex: 1, data: adjustOverlap(pulseData, 'pulse'),
symbol: 'circle', symbolSize: 10, itemStyle: { color: '#f5222d' },
lineStyle: { color: '#f5222d', width: 2 }, connectNulls: false
}
]
}
chartInstance.setOption(option, true)
// 同步下方表格数据
tableData.value = sorted.map(d => ({
time: d.timeStr,
temperature: d.temperature,
heartRate: d.heartRate,
pulse: d.pulse
}))
}
const fetchData = async () => {
try {
const res = await getVitalSigns(props.patientId)
renderChart(res.data || [])
} catch (e) {
console.error('获取体征数据失败', e)
}
}
onMounted(() => {
initChart()
fetchData()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
})
// 暴露刷新方法供父组件在保存成功后调用
defineExpose({ refresh: fetchData })
</script>
<style scoped>
.vital-signs-container { padding: 10px; background: #fff; border-radius: 4px; }
.vital-signs-chart { width: 100%; }
</style>

View File

@@ -60,3 +60,40 @@ describe('Bug #595: 住院护士站-医嘱校对列表字段完整性与皮试
cy.contains('th', '频次/用法').should('exist')
})
})
// Bug #566 Regression Test
describe('Bug #566: 住院护士站-三测单图表渲染与数据同步', { tags: ['@bug566', '@regression'] }, () => {
it('录入体征数据后,体温单图表区应自动渲染数据点、连线,且下方表格同步显示', () => {
cy.login('wx', '123456')
cy.visit('/inpatient/vital-signs')
// 1. 选中患者并新增体征数据
cy.get('.patient-list .el-table__row').first().click()
cy.get('.el-button').contains('新增').click()
cy.get('.el-dialog__body').should('be.visible')
cy.get('input[placeholder*="日期"]').type('2026-05-20')
cy.get('input[placeholder*="时间"]').type('06:00')
cy.get('input[placeholder*="体温"]').type('38.6')
cy.get('input[placeholder*="心率"]').type('89')
cy.get('input[placeholder*="脉搏"]').type('45')
cy.get('.el-dialog__footer .el-button--primary').contains('保存').click()
// 2. 验证弹窗关闭且提示成功
cy.get('.el-message').contains('保存成功').should('exist')
cy.get('.el-dialog').should('not.exist')
// 3. 验证表格区同步显示
cy.get('.vital-signs-table .el-table__body-wrapper').should('be.visible')
cy.contains('td', '38.6').should('exist')
cy.contains('td', '89').should('exist')
cy.contains('td', '45').should('exist')
// 4. 验证图表区渲染 (ECharts canvas)
cy.get('.vital-signs-chart canvas').should('be.visible')
// 模拟鼠标悬停验证数据点存在
cy.get('.vital-signs-chart').trigger('mousemove', { clientX: 500, clientY: 300 })
cy.get('.echarts-tooltip').should('be.visible')
cy.contains('.echarts-tooltip', '38.6').should('exist')
})
})