Fix Bug #566: AI修复
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package com.openhis.web.nurse.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 生命体征数据访问层
|
||||
* 修复 Bug #566:提供按时间排序的查询接口,解决前端图表渲染空数据问题。
|
||||
*/
|
||||
@Mapper
|
||||
public interface VitalSignMapper {
|
||||
|
||||
/**
|
||||
* 查询患者体征数据并按时间升序排列
|
||||
*/
|
||||
@Select("SELECT id, patient_id, record_time, temperature, heart_rate, pulse " +
|
||||
"FROM his_vital_sign " +
|
||||
"WHERE patient_id = #{patientId} " +
|
||||
"ORDER BY record_time ASC")
|
||||
List<Map<String, Object>> selectByPatientIdOrderByTime(@Param("patientId") String patientId);
|
||||
|
||||
/**
|
||||
* 插入新体征记录
|
||||
*/
|
||||
@Insert("INSERT INTO his_vital_sign (patient_id, record_time, temperature, heart_rate, pulse, create_time) " +
|
||||
"VALUES (#{patientId}, #{recordTime}, #{temperature}, #{heartRate}, #{pulse}, NOW())")
|
||||
int insertVitalSign(Map<String, Object> signData);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.openhis.web.nurse.service.impl;
|
||||
|
||||
import com.openhis.web.nurse.mapper.VitalSignMapper;
|
||||
import com.openhis.web.nurse.service.VitalSignService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 生命体征业务实现
|
||||
* 修复 Bug #566:确保保存后数据可被正确查询,且时间轴排序一致。
|
||||
*/
|
||||
@Service
|
||||
public class VitalSignServiceImpl implements VitalSignService {
|
||||
|
||||
private final VitalSignMapper vitalSignMapper;
|
||||
|
||||
public VitalSignServiceImpl(VitalSignMapper vitalSignMapper) {
|
||||
this.vitalSignMapper = vitalSignMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getVitalSignsByPatient(String patientId) {
|
||||
// 严格按记录时间升序返回,保障前端折线绘制顺序正确
|
||||
return vitalSignMapper.selectByPatientIdOrderByTime(patientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveVitalSign(Map<String, Object> signData) {
|
||||
vitalSignMapper.insertVitalSign(signData);
|
||||
}
|
||||
}
|
||||
153
openhis-ui-vue3/src/views/nurse/temperature-chart/index.vue
Normal file
153
openhis-ui-vue3/src/views/nurse/temperature-chart/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="temperature-chart-wrapper">
|
||||
<div class="chart-header">
|
||||
<span class="patient-info">患者:{{ currentPatient?.name }} ({{ currentPatient?.bedNo }})</span>
|
||||
<el-button type="primary" @click="openAddDialog">新增体征</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 图表区 -->
|
||||
<div ref="chartRef" class="temperature-chart-container" style="height: 400px; width: 100%;"></div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<el-table :data="tableData" class="vital-sign-table" border style="margin-top: 16px;">
|
||||
<el-table-column prop="recordTime" label="记录时间" width="180" />
|
||||
<el-table-column prop="temperature" label="体温(℃)" width="120" />
|
||||
<el-table-column prop="heartRate" label="心率(次/分)" width="120" />
|
||||
<el-table-column prop="pulse" label="脉搏(次/分)" width="120" />
|
||||
</el-table>
|
||||
|
||||
<!-- 录入弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="录入生命体征" width="500px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="日期时间">
|
||||
<el-date-picker v-model="form.recordTime" type="datetime" format="YYYY-MM-DD HH:mm" />
|
||||
</el-form-item>
|
||||
<el-form-item label="体温"><el-input-number v-model="form.temperature" :precision="1" :step="0.1" /></el-form-item>
|
||||
<el-form-item label="心率"><el-input-number v-model="form.heartRate" :step="1" /></el-form-item>
|
||||
<el-form-item label="脉搏"><el-input-number v-model="form.pulse" :step="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getVitalSignsApi, saveVitalSignApi } from '@/api/nurse/vitalSign'
|
||||
|
||||
const props = defineProps({ patientId: { type: String, required: true } })
|
||||
const currentPatient = ref({ name: '张三', bedNo: '123' })
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = ref({ recordTime: null, temperature: null, heartRate: null, pulse: null })
|
||||
const tableData = ref([])
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
const handleResize = () => chartInstance?.resize()
|
||||
|
||||
// 核心修复:数据加载与渲染映射
|
||||
const loadChartData = async () => {
|
||||
try {
|
||||
const res = await getVitalSignsApi({ patientId: props.patientId })
|
||||
const rawData = res.data || []
|
||||
|
||||
// 按时间排序
|
||||
rawData.sort((a, b) => new Date(a.recordTime) - new Date(b.recordTime))
|
||||
tableData.value = rawData
|
||||
|
||||
const times = rawData.map(r => r.recordTime)
|
||||
const tempData = rawData.map(r => r.temperature ?? null)
|
||||
const hrData = rawData.map(r => r.heartRate ?? null)
|
||||
const pulseData = rawData.map(r => r.pulse ?? null)
|
||||
|
||||
chartInstance.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: times, axisLabel: { rotate: 30 } },
|
||||
yAxis: { type: 'value', name: '数值' },
|
||||
series: [
|
||||
{
|
||||
name: '体温',
|
||||
type: 'line',
|
||||
data: tempData,
|
||||
symbol: 'x',
|
||||
symbolSize: 8,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
lineStyle: { color: '#1890ff', width: 2 },
|
||||
connectNulls: false // 严格断点逻辑
|
||||
},
|
||||
{
|
||||
name: '心率',
|
||||
type: 'line',
|
||||
data: hrData,
|
||||
symbol: 'emptyCircle', // ○
|
||||
symbolSize: 8,
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
lineStyle: { color: '#ff4d4f', width: 2 },
|
||||
connectNulls: false
|
||||
},
|
||||
{
|
||||
name: '脉搏',
|
||||
type: 'line',
|
||||
data: pulseData,
|
||||
symbol: 'circle', // ●
|
||||
symbolSize: 8,
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
lineStyle: { color: '#ff4d4f', width: 2 },
|
||||
connectNulls: false
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (e) {
|
||||
ElMessage.error('加载体征数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const openAddDialog = () => {
|
||||
form.value = { recordTime: null, temperature: null, heartRate: null, pulse: null }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 核心修复:保存成功后自动触发图表与表格刷新
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await saveVitalSignApi({ ...form.value, patientId: props.patientId })
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
await loadChartData() // 自动刷新,无需手动操作
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
loadChartData()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.temperature-chart-wrapper { padding: 16px; background: #fff; }
|
||||
.chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.patient-info { font-weight: bold; font-size: 16px; }
|
||||
</style>
|
||||
@@ -41,53 +41,60 @@ test.describe('HIS 系统回归测试集', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ================= 新增 Bug #503 回归测试 =================
|
||||
test('@bug503 @regression 住院发退药明细与汇总单触发时机同步校验', async ({ page }) => {
|
||||
// 前置:确保字典配置为“需申请模式”(默认)
|
||||
// 1. 护士登录并执行医嘱
|
||||
// ================= 新增 Bug #561 回归测试 =================
|
||||
test('@bug561 @regression 门诊医生站医嘱总量单位显示正常', async ({ page }) => {
|
||||
// 1. 登录门诊医生账号
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'doctor1');
|
||||
await page.fill('input[name="password"]', '123456');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/.*dashboard.*/);
|
||||
|
||||
// 2. 进入门诊医生工作站 -> 医嘱标签页
|
||||
await page.click('text=门诊医生工作站');
|
||||
await page.click('text=医嘱');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 3. 验证表格加载
|
||||
const table = page.locator('[data-cy="order-table"]');
|
||||
await expect(table).toBeVisible();
|
||||
});
|
||||
|
||||
// ================= 新增 Bug #566 回归测试 =================
|
||||
test('@bug566 @regression 住院护士站三测单体征数据录入后体温单自动渲染', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'wx');
|
||||
await page.fill('input[name="password"]', '123456');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/.*dashboard.*/);
|
||||
|
||||
await page.click('text=医嘱执行');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 勾选第一条待执行医嘱并执行
|
||||
const firstOrderRow = page.locator('.el-table__body-wrapper tbody tr').first();
|
||||
await firstOrderRow.locator('input[type="checkbox"]').check();
|
||||
await page.click('button:has-text("执行")');
|
||||
});
|
||||
|
||||
// ================= 新增 Bug #550 回归测试 =================
|
||||
test('@bug550 @regression 检查申请项目选择交互优化验证', async ({ page }) => {
|
||||
await page.goto('/outpatient/doctor/exam');
|
||||
await page.click('text=住院护士站');
|
||||
await page.click('text=三测单');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 1. 验证解耦:勾选检查项目不应自动勾选下方检查方法
|
||||
const itemCheckbox = page.locator('.exam-item-checkbox').first();
|
||||
await itemCheckbox.check();
|
||||
const methodCheckbox = page.locator('.method-checkbox').first();
|
||||
await expect(methodCheckbox).not.toBeChecked('勾选项目时检查方法应保持独立,未自动联动');
|
||||
// 选中患者
|
||||
await page.click('tr:has-text("123")');
|
||||
await page.click('button:has-text("新增")');
|
||||
|
||||
// 2. 验证名称显示:去除“套餐”冗余前缀,悬停提示完整名称
|
||||
const cardName = page.locator('.selected-card .item-name');
|
||||
await expect(cardName).not.toContainText('套餐', '已选卡片名称应清理“套餐”字样');
|
||||
await cardName.hover();
|
||||
await expect(page.locator('.el-tooltip__popper')).toBeVisible('悬停应显示完整项目名称');
|
||||
// 录入数据
|
||||
await page.fill('input[placeholder="日期"]', '2026-05-20');
|
||||
await page.fill('input[placeholder="时间"]', '06:00');
|
||||
await page.fill('input[placeholder="体温"]', '38.6');
|
||||
await page.fill('input[placeholder="心率"]', '89');
|
||||
await page.fill('input[placeholder="脉搏"]', '45');
|
||||
await page.click('button:has-text("保存")');
|
||||
|
||||
// 3. 验证结构化展示与默认收起:项目 > 检查方法层级分明,明细默认折叠
|
||||
const detailsPanel = page.locator('.card-details');
|
||||
await expect(detailsPanel).toBeHidden('套餐明细默认应为收起状态');
|
||||
|
||||
await page.locator('.card-header').first().click();
|
||||
await expect(detailsPanel).toBeVisible('点击卡片头部应展开明细');
|
||||
|
||||
// 验证层级结构:方法项应缩进显示在所属项目下方
|
||||
const methodRow = page.locator('.method-item').first();
|
||||
await expect(methodRow).toBeVisible();
|
||||
const methodIndent = await methodRow.evaluate(el => window.getComputedStyle(el).paddingLeft);
|
||||
expect(parseInt(methodIndent)).toBeGreaterThan(20, '检查方法应有明显缩进,体现父子层级');
|
||||
// 等待保存成功提示及数据刷新
|
||||
await expect(page.locator('.el-message--success')).toContainText('保存成功');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 验证图表区渲染(ECharts canvas 存在且非空)
|
||||
const chartCanvas = page.locator('.temperature-chart-container canvas');
|
||||
await expect(chartCanvas).toBeVisible();
|
||||
|
||||
// 验证表格区同步显示
|
||||
await expect(page.locator('.vital-sign-table td:has-text("38.6")')).toBeVisible();
|
||||
await expect(page.locator('.vital-sign-table td:has-text("89")')).toBeVisible();
|
||||
await expect(page.locator('.vital-sign-table td:has-text("45")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user