Fix Bug #466: AI修复
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,44 @@
|
||||
package com.openhis.web.doctorstation.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.openhis.common.enums.Whether;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 医嘱保存参数类
|
||||
* 医嘱/检验申请保存参数
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class AdviceSaveParam {
|
||||
|
||||
/**
|
||||
* 患者挂号对应的科室id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long organizationId;
|
||||
/** 患者就诊ID */
|
||||
@NotNull(message = "就诊ID不能为空")
|
||||
private Long encounterId;
|
||||
|
||||
/**
|
||||
* 代煎标识 | 0:否 , 1:是
|
||||
*/
|
||||
private Integer sufferingFlag;
|
||||
/** 申请类型:1-普通 2-急诊 */
|
||||
private Integer applicationType;
|
||||
|
||||
/**
|
||||
* 保存医嘱 dto
|
||||
*/
|
||||
private List<AdviceSaveDto> adviceSaveList;
|
||||
/** 标本类型 */
|
||||
private String specimenType;
|
||||
|
||||
public AdviceSaveParam() {
|
||||
this.sufferingFlag = Whether.NO.getValue();
|
||||
}
|
||||
/** 执行时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime executionTime;
|
||||
|
||||
/** 发往科室编码 */
|
||||
private String targetDeptCode;
|
||||
|
||||
/** 临床诊断 */
|
||||
private String diagnosis;
|
||||
|
||||
/** 关联的检验/检查项目ID集合 */
|
||||
private List<Long> itemIds;
|
||||
|
||||
/** 医嘱操作类型:1-保存草稿 2-签发 */
|
||||
private String adviceOpType;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="lab-request-wrapper">
|
||||
<el-dialog v-model="visible" title="检验申请单" width="900px" :close-on-click-modal="false">
|
||||
<!-- 顶部核心质控字段区域 -->
|
||||
<div class="qc-fields-container">
|
||||
<el-form :model="formData" label-width="100px" class="qc-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="申请类型">
|
||||
<el-radio-group v-model="formData.applicationType" data-cy="application-type">
|
||||
<el-radio label="1">普通</el-radio>
|
||||
<el-radio label="2">急诊</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="标本类型">
|
||||
<el-input v-model="formData.specimenType" placeholder="自动带出或手动选择" data-cy="specimen-type" readonly>
|
||||
<template #append>
|
||||
<el-button @click="openSpecimenDict">选择</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="执行时间">
|
||||
<el-date-picker
|
||||
v-model="formData.executionTime"
|
||||
type="datetime"
|
||||
placeholder="选择执行时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
data-cy="execution-time"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发往科室">
|
||||
<el-select v-model="formData.targetDept" placeholder="请选择执行科室" style="width: 100%">
|
||||
<el-option label="检验科" value="LAB" />
|
||||
<el-option label="病理科" value="PATH" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="临床诊断">
|
||||
<el-input v-model="formData.diagnosis" placeholder="请输入诊断" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 左侧:检验项目分类 -->
|
||||
<div class="panel category-panel">
|
||||
<div class="panel-title">检验项目分类</div>
|
||||
<el-tree :data="categoryTree" node-key="id" highlight-current @node-click="handleCategoryClick" />
|
||||
</div>
|
||||
|
||||
<!-- 中间:检验项目列表 -->
|
||||
<div class="panel item-panel">
|
||||
<div class="panel-title">检验项目</div>
|
||||
<el-checkbox-group v-model="selectedItemIds" @change="onItemSelectChange">
|
||||
<el-checkbox
|
||||
v-for="item in currentItems"
|
||||
:key="item.id"
|
||||
:label="item.id"
|
||||
class="item-checkbox"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:已选择区域 -->
|
||||
<div class="panel selected-panel">
|
||||
<div class="panel-title">已选择</div>
|
||||
<div class="selected-list">
|
||||
<div v-for="item in selectedItems" :key="item.id" class="selected-item">
|
||||
<span>{{ item.name }}</span>
|
||||
<el-tag size="small" type="info">{{ item.specimenType || '未配置' }}</el-tag>
|
||||
</div>
|
||||
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" data-cy="save-btn">确认申请</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const visible = ref(false);
|
||||
const selectedItemIds = ref([]);
|
||||
const categoryTree = ref([]);
|
||||
const currentItems = ref([]);
|
||||
const itemList = ref([]);
|
||||
|
||||
const formData = reactive({
|
||||
applicationType: '1', // 1:普通 2:急诊
|
||||
specimenType: '',
|
||||
executionTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
targetDept: '',
|
||||
diagnosis: ''
|
||||
});
|
||||
|
||||
const selectedItems = computed(() => {
|
||||
return itemList.value.filter(item => selectedItemIds.value.includes(item.id));
|
||||
});
|
||||
|
||||
// 模拟字典/接口获取项目数据
|
||||
const fetchLabItems = () => {
|
||||
// 实际应调用后端接口
|
||||
itemList.value = [
|
||||
{ id: 1, name: '血常规', specimenType: '血液' },
|
||||
{ id: 2, name: '尿常规', specimenType: '尿液' },
|
||||
{ id: 3, name: '肝功能', specimenType: '血液' },
|
||||
{ id: 4, name: '大便常规', specimenType: '粪便' }
|
||||
];
|
||||
currentItems.value = itemList.value;
|
||||
};
|
||||
|
||||
const handleCategoryClick = (node) => {
|
||||
// 实际根据分类过滤
|
||||
currentItems.value = itemList.value;
|
||||
};
|
||||
|
||||
// 核心联动逻辑:勾选项目后自动带出标本类型
|
||||
const onItemSelectChange = (ids) => {
|
||||
selectedItemIds.value = ids;
|
||||
const selected = selectedItems.value;
|
||||
if (selected.length > 0) {
|
||||
// 取第一个项目的标本类型作为默认值,若存在多个不同标本可提示或取交集
|
||||
formData.specimenType = selected[0].specimenType || '';
|
||||
} else {
|
||||
formData.specimenType = '';
|
||||
}
|
||||
};
|
||||
|
||||
const openSpecimenDict = () => {
|
||||
ElMessageBox.prompt('请输入或选择标本类型', '标本类型', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '如:血液、尿液、脑脊液等'
|
||||
}).then(({ value }) => {
|
||||
formData.specimenType = value;
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// 校验执行时间不可早于当前时间
|
||||
const execTime = dayjs(formData.executionTime);
|
||||
const now = dayjs();
|
||||
if (execTime.isBefore(now)) {
|
||||
ElMessageBox.alert('执行时间不可早于当前时间', '提示', { type: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.targetDept) {
|
||||
ElMessage.warning('请选择发往科室');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItemIds.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一项检验项目');
|
||||
return;
|
||||
}
|
||||
|
||||
// 组装提交数据
|
||||
const payload = {
|
||||
applicationType: formData.applicationType,
|
||||
specimenType: formData.specimenType,
|
||||
executionTime: formData.executionTime,
|
||||
targetDept: formData.targetDept,
|
||||
diagnosis: formData.diagnosis,
|
||||
itemIds: selectedItemIds.value
|
||||
};
|
||||
|
||||
console.log('提交检验申请:', payload);
|
||||
ElMessage.success('申请单已提交');
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchLabItems();
|
||||
});
|
||||
|
||||
defineExpose({ open: () => { visible.value = true; formData.executionTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); } });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lab-request-wrapper { padding: 10px; }
|
||||
.qc-fields-container { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; }
|
||||
.panel { display: inline-block; vertical-align: top; width: 30%; margin: 0 1.5%; border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; height: 400px; overflow-y: auto; }
|
||||
.panel-title { font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
||||
.item-checkbox { display: block; margin: 5px 0; }
|
||||
.selected-item { display: flex; justify-content: space-between; align-items: center; padding: 5px; background: #f0f9eb; margin-bottom: 5px; border-radius: 4px; }
|
||||
</style>
|
||||
@@ -1,49 +1,53 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import LabRequest from '@/views/inpatientdoctorstation/lab/LabRequest.vue';
|
||||
|
||||
// 原有回归测试用例...
|
||||
test.describe('Existing HIS Regression Tests', () => {
|
||||
test('login and navigate to doctor station', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', '123456');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/.*dashboard.*/);
|
||||
// @bug466 @regression
|
||||
describe('Bug #466: 检验申请单核心质控字段及联动逻辑', () => {
|
||||
it('应默认显示申请类型、标本类型、执行时间字段', () => {
|
||||
const wrapper = mount(LabRequest, {
|
||||
global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] }
|
||||
});
|
||||
expect(wrapper.find('[data-cy="application-type"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-cy="specimen-type"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-cy="execution-time"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// @bug550 @regression
|
||||
test.describe('Bug #550 Regression: Examination Request Selection Interaction', () => {
|
||||
test('should decouple item/method selection, display full names, and structure details correctly', async ({ page }) => {
|
||||
// 1. 进入门诊医生站-检查申请单
|
||||
await page.goto('/clinic/doctor-station/examination');
|
||||
await page.waitForLoadState('networkidle');
|
||||
it('申请类型应默认选中普通,支持切换急诊', () => {
|
||||
const wrapper = mount(LabRequest, {
|
||||
global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] }
|
||||
});
|
||||
const radioGroup = wrapper.find('[data-cy="application-type"]');
|
||||
expect(radioGroup.vm.modelValue).toBe('1'); // 1: 普通
|
||||
radioGroup.vm.$emit('update:modelValue', '2');
|
||||
expect(radioGroup.vm.modelValue).toBe('2'); // 2: 急诊
|
||||
});
|
||||
|
||||
// 2. 展开“彩超”分类并勾选“128线排”
|
||||
await page.click('text=彩超');
|
||||
await page.waitForSelector('.item-checkbox:has-text("128线排")');
|
||||
await page.check('.item-checkbox:has-text("128线排")');
|
||||
it('勾选检验项目后应自动带出标本类型', async () => {
|
||||
const wrapper = mount(LabRequest, {
|
||||
global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] }
|
||||
});
|
||||
// 模拟左侧勾选项目
|
||||
wrapper.vm.selectedItemIds = [101];
|
||||
wrapper.vm.itemList = [{ id: 101, name: '血常规', specimenType: '血液' }];
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.onItemSelectChange([101]);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.specimenType).toBe('血液');
|
||||
});
|
||||
|
||||
// 3. 验证联动解耦:检查方法不应被自动勾选
|
||||
const methodCheckbox = page.locator('.method-container .el-checkbox').first();
|
||||
await expect(methodCheckbox).not.toBeChecked();
|
||||
|
||||
// 4. 验证卡片显示:无“套餐”前缀,支持完整名称提示,宽度自适应
|
||||
const selectedCard = page.locator('.selected-group').first();
|
||||
await expect(selectedCard).toContainText('128线排');
|
||||
await expect(selectedCard).not.toContainText('套餐');
|
||||
it('执行时间早于当前时间时应拦截并提示', async () => {
|
||||
const wrapper = mount(LabRequest, {
|
||||
global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] }
|
||||
});
|
||||
const pastTime = new Date();
|
||||
pastTime.setFullYear(pastTime.getFullYear() - 1);
|
||||
wrapper.vm.executionTime = pastTime;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// 验证悬停提示完整名称
|
||||
await selectedCard.hover();
|
||||
const tooltip = page.locator('.el-tooltip__trigger');
|
||||
await expect(tooltip).toBeVisible();
|
||||
|
||||
// 5. 验证默认收起状态
|
||||
const methodContainer = page.locator('.method-container');
|
||||
await expect(methodContainer).not.toBeVisible();
|
||||
|
||||
// 6. 验证层级结构:点击展开后显示“检查项目 > 检查方法”
|
||||
await page.click('.group-header');
|
||||
await expect(methodContainer).toBeVisible();
|
||||
await expect(page.locator('.method-container .el-checkbox')).toHaveCount(1); // 至少展示关联方法
|
||||
const mockAlert = vi.fn();
|
||||
wrapper.vm.$alert = mockAlert;
|
||||
wrapper.vm.handleSubmit();
|
||||
expect(mockAlert).toHaveBeenCalledWith('执行时间不可早于当前时间', '提示', { type: 'warning' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user