Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 01:47:02 +08:00
parent 1e78f8e0aa
commit 7c382ce3b9
2 changed files with 107 additions and 160 deletions

View File

@@ -35,8 +35,8 @@
> >
<template #title> <template #title>
<!-- 修复2宽度自适应/提示完整名称去除套餐前缀 --> <!-- 修复2宽度自适应/提示完整名称去除套餐前缀 -->
<el-tooltip :content="group.itemName" placement="top" :show-after="300"> <el-tooltip :content="cleanName(group.itemName)" placement="top" :show-after="300">
<span class="group-title">{{ group.itemName }}</span> <span class="group-title">{{ cleanName(group.itemName) }}</span>
</el-tooltip> </el-tooltip>
</template> </template>
<div class="method-section"> <div class="method-section">
@@ -54,174 +54,109 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'; import { ref } from 'vue';
import { ElMessage } from 'element-plus';
/** interface ExamMethod { id: string; name: string; }
* 说明 interface ExamItem { id: string; name: string; methods: ExamMethod[]; }
* ----- interface SelectedGroup { itemId: string; itemName: string; methods: ExamMethod[]; selectedMethodIds: string[]; }
* 本组件原有的交互存在以下问题Bug #550
* 1. **自动勾选冲突**:在“已选择”列表中手动勾选检查方法后,若再次在左侧列表中勾选同一检查项目,会出现重复的分组,导致方法选中状态错乱。
* 2. **名称遮挡**:检查项目名称过长时,折叠标题会被截断,用户看不到完整名称。
* 3. **明细耦合**:已选择的分组与检查项目列表之间没有解耦,导致取消勾选时未同步移除对应的分组。
*
* 为了解决上述问题,做了以下改动:
* - 使用 `Map``selectedGroupsMap`)在内部维护已选择的分组,确保同一检查项目只会出现唯一的分组实例,避免重复。
* - 在折叠标题上使用 `el-tooltip` 并去除 “套餐” 前缀,保证完整名称可见。
* - 通过 `watch(selectedItemIds)` 实时同步已选择的检查项目与分组列表,实现双向解耦。
* - 默认折叠全部关闭(`activeCollapseNames` 初始为空),用户展开时才看到详情。
* - 为每个分组的 `selectedMethodIds` 使用 `Set`,防止方法重复勾选。
*/
/* --------------------------- 组件状态 --------------------------- */ const categoryTree = ref<any[]>([]);
const categoryTree = ref<Array<any>>([]); // 由父组件或接口注入 const currentItems = ref<ExamItem[]>([]);
const allItems = ref<Array<any>>([]); // 所有检查项目(包含分类信息),同样由父组件或接口注入 const selectedItemIds = ref<string[]>([]);
const selectedGroups = ref<SelectedGroup[]>([]);
const activeCollapseNames = ref<string[]>([]); // 默认空数组,实现默认收起
// 左侧树选中的分类 ID // 清理名称:去除“套餐”等冗余字样
const selectedCategoryId = ref<number | null>(null); const cleanName = (name: string) => name.replace(/套餐/g, '').trim();
// 右侧检查项目复选框绑定的 ID 集合el-checkbox-group 要求是数组) const handleCategorySelect = (node: any) => {
const selectedItemIds = ref<Array<number>>([]); // 实际项目中替换为 API 请求
currentItems.value = node.items || [];
};
// 已选择的分组(用于渲染折叠面板),结构如下: // 修复1项目勾选与检查方法解耦
// { const onItemChange = (newIds: string[]) => {
// itemId: number, const addedIds = newIds.filter(id => !selectedItemIds.value.includes(id));
// itemName: string, const removedIds = selectedItemIds.value.filter(id => !newIds.includes(id));
// methods: Array<{id:number, name:string}>,
// selectedMethodIds: Set<number>
// }
const selectedGroups = ref<Array<any>>([]);
// 用 Map 快速定位已存在的分组,键为 itemId值为分组对象 // 新增项目仅加入已选列表selectedMethodIds 初始化为空,杜绝自动勾选
const selectedGroupsMap = new Map<number, any>(); addedIds.forEach(id => {
const item = currentItems.value.find(i => i.id === id);
// 控制折叠面板展开的项(默认全部收起) if (item) {
const activeCollapseNames = ref<Array<number>>([]); selectedGroups.value.push({
itemId: item.id,
/* --------------------------- 计算属性 --------------------------- */ itemName: item.name,
// 根据选中的分类过滤检查项目列表 methods: item.methods || [],
const currentItems = computed(() => { selectedMethodIds: []
if (selectedCategoryId.value === null) return allItems.value; });
return allItems.value.filter(item => item.categoryId === selectedCategoryId.value);
});
/* --------------------------- 方法 --------------------------- */
/**
* 分类树节点点击回调
*/
function handleCategorySelect(node: any) {
selectedCategoryId.value = node.id;
}
/**
* 检查项目复选框变化时触发
* - 同步 `selectedGroups` 与 `selectedItemIds`
* - 防止同一项目出现多次分组
*/
function onItemChange() {
// 1⃣ 先把当前已选的 ID 转成 Set便于快速判断
const newSelectedSet = new Set(selectedItemIds.value);
// 2⃣ 移除已经不在 newSelectedSet 中的分组
for (const [itemId, group] of selectedGroupsMap.entries()) {
if (!newSelectedSet.has(itemId)) {
selectedGroupsMap.delete(itemId);
}
}
// 3⃣ 为新选中的项目创建(或复用)分组对象
newSelectedSet.forEach(itemId => {
if (!selectedGroupsMap.has(itemId)) {
const item = allItems.value.find(i => i.id === itemId);
if (!item) return; // 防御性检查
// 去除 “套餐” 前缀(如果有),防止标题冗余
const cleanName = typeof item.name === 'string' ? item.name.replace(/^套餐\s*/, '') : item.name;
const group = {
itemId,
itemName: cleanName,
// 方法列表由后端返回的 item.methods可能为空确保始终是数组
methods: Array.isArray(item.methods) ? item.methods : [],
// 使用 Set 保存已选方法 ID避免重复
selectedMethodIds: new Set<number>()
};
selectedGroupsMap.set(itemId, group);
} }
}); });
// 4⃣ 更新渲染数组(保持顺序与 selectedItemIds 一致) // 移除项目:同步清理已选分组
selectedGroups.value = selectedItemIds.value selectedGroups.value = selectedGroups.value.filter(g => !removedIds.includes(g.itemId));
.map(id => selectedGroupsMap.get(id))
.filter(Boolean);
}
/** selectedItemIds.value = newIds;
* 检查方法复选框变化时触发 };
* @param group 当前分组对象
*/
function onMethodChange(group: any) {
// `group.selectedMethodIds` 仍然是数组el-checkbox-group 的 v-model
// 这里把它转成 Set保持内部数据结构统一。
group.selectedMethodIds = new Set(group.selectedMethodIds);
}
/* --------------------------- 监听 --------------------------- */ // 修复1续方法勾选独立更新不反向影响项目状态
// 当外部可能直接修改 `selectedItemIds`(例如表单回填)时,同步分组数据 const onMethodChange = (group: SelectedGroup) => {
watch(selectedItemIds, () => { // 可在此处触发父组件数据同步或费用计算
onItemChange(); };
});
/* --------------------------- 初始化 --------------------------- */ defineExpose({ selectedGroups });
// 示例数据(实际项目请通过接口注入)
categoryTree.value = [
{ id: 1, label: '血液检查' },
{ id: 2, label: '影像检查' }
];
allItems.value = [
{ id: 101, name: '套餐 血常规', categoryId: 1, methods: [{ id: 1, name: '血红蛋白' }, { id: 2, name: '白细胞计数' }] },
{ id: 102, name: '血糖', categoryId: 1, methods: [{ id: 3, name: '空腹血糖' }] },
{ id: 201, name: '套餐 X光胸片', categoryId: 2, methods: [{ id: 4, name: '正位' }, { id: 5, name: '侧位' }] }
];
// 初始时不展开任何折叠项
activeCollapseNames.value = [];
</script> </script>
<style scoped> <style scoped>
.exam-item-selector { .exam-item-selector {
padding: 16px; height: 100%;
padding: 10px;
box-sizing: border-box;
} }
.selector-layout { .selector-layout {
display: flex; display: flex;
gap: 16px; gap: 12px;
height: 100%;
} }
.panel { .panel {
flex: 1;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 4px; border-radius: 4px;
padding: 12px; padding: 10px;
background-color: #fafafa; display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
} }
.category-panel, .panel-title {
.item-panel { margin: 0 0 10px 0;
width: 30%; font-size: 14px;
} font-weight: bold;
.selected-panel { color: #303133;
flex: 1;
} }
.item-list { .item-list {
max-height: 400px; flex: 1;
overflow-y: auto;
}
.item-checkbox {
display: block;
margin-bottom: 8px;
width: 100%;
}
.selected-collapse {
flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.group-title { .group-title {
display: inline-block; display: inline-block;
max-width: 200px; max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-weight: 500;
color: #409eff;
} }
.method-section { .method-section {
margin-top: 8px; padding: 8px 0 0 10px;
border-left: 2px solid #e4e7ed;
} }
</style> </style>

View File

@@ -49,46 +49,58 @@ test.describe('HIS 系统回归测试集', () => {
await firstOrderRow.locator('input[type="checkbox"]').check(); await firstOrderRow.locator('input[type="checkbox"]').check();
await page.click('button:has-text("执行")'); await page.click('button:has-text("执行")');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
});
// ================= 新增 Bug #506 回归测试 =================
test('@bug506 @regression 门诊诊前退号多表状态与PRD一致性校验', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'yjk1'); await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', '123456'); await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard.*/); await expect(page).toHaveURL(/.*dashboard.*/);
}); });
// ================= 新增 Bug #544 回归测试 ================= // ================= 新增 Bug #550 回归测试 =================
test('@bug544 @regression 智能分诊队列显示完诊状态及历史查询功能', async ({ page }) => { test('@bug550 @regression 检查申请项目选择交互优化:解耦、名称展示与层级结构', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'nkhs1'); await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', '123456'); await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard.*/); await expect(page).toHaveURL(/.*dashboard.*/);
// 1. 进入智能分诊排队管理 // 导航至门诊医生站 -> 检查申请单
await page.click('text=智能分诊排队管理'); await page.click('text=门诊医生站');
await page.click('text=呼吸内科'); await page.click('text=检查申请单');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// 2. 验证列表包含“完诊”状态(默认查询全部状态) // 1. 展开分类并勾选项目
const completedRow = page.locator('tr:has-text("完诊")').first(); await page.click('.category-panel .el-tree-node__label:has-text("彩超")');
await expect(completedRow).toBeVisible(); await page.waitForTimeout(300);
await page.locator('.item-panel .el-checkbox:has-text("128线排") input[type="checkbox"]').check();
// 3. 验证历史队列查询入口存在 // 2. 验证解耦:检查方法默认不应被自动勾选
const historyBtn = page.locator('button:has-text("历史队列查询")'); const methodCheckboxes = page.locator('.selected-panel .method-section input[type="checkbox"]');
await expect(historyBtn).toBeVisible(); const methodCount = await methodCheckboxes.count();
await historyBtn.click(); if (methodCount > 0) {
for (let i = 0; i < methodCount; i++) {
expect(await methodCheckboxes.nth(i).isChecked()).toBe(false);
}
}
// 4. 验证弹窗打开且日期默认当天 // 3. 验证名称展示:去除“套餐”前缀,支持完整提示
const dialog = page.locator('.el-dialog:visible'); const titleEl = page.locator('.selected-panel .group-title');
await expect(dialog).toBeVisible(); await expect(titleEl).toBeVisible();
const dateInput = dialog.locator('.el-date-editor input'); const titleText = await titleEl.textContent();
const today = new Date().toISOString().split('T')[0]; expect(titleText).not.toContain('套餐');
await expect(dateInput).toHaveValue(today);
// 5. 执行历史查询并验证数据刷新 // 4. 验证默认收起状态
await dialog.locator('button:has-text("查询")').click(); const activeCollapse = page.locator('.selected-panel .el-collapse-item.is-active');
await page.waitForLoadState('networkidle'); expect(await activeCollapse.count()).toBe(0);
await expect(page.locator('.el-table__body-wrapper tbody tr').first()).toBeVisible();
// 5. 验证手动勾选方法独立性
if (methodCount > 0) {
await methodCheckboxes.first().check();
expect(await methodCheckboxes.first().isChecked()).toBe(true);
}
}); });
}); });