Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 02:01:20 +08:00
parent a23ec8026a
commit 31e35e7c1a
5 changed files with 278 additions and 118 deletions

View File

@@ -1,124 +1,272 @@
<template>
<div class="check-request">
<el-table
ref="table"
:data="requestList"
style="width: 100%"
@selection-change="handleSelectionChange"
:row-key="row => row.id"
:default-expand-all="true"
>
<!-- 解决名称遮挡:使用 tooltip 并限制宽度 -->
<el-table-column
prop="itemName"
label="检查项目"
width="200"
:show-overflow-tooltip="true"
>
<template #default="{ row }">
<el-tooltip
class="item"
effect="dark"
:content="row.itemName"
placement="top-start"
>
<span class="ellipsis">{{ row.itemName }}</span>
</el-tooltip>
</template>
</el-table-column>
<div class="check-request-container">
<div class="layout-wrapper">
<!-- 左侧检查项目分类 -->
<div class="panel left-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
@node-click="handleCategoryClick"
highlight-current
default-expand-all
class="category-tree"
/>
</div>
<!-- 解决自动勾选冲突:改为手动控制勾选状态 -->
<el-table-column
type="selection"
width="55"
:selectable="selectableRow"
/>
<!-- 中间检查项目列表 -->
<div class="panel middle-panel">
<h3 class="panel-title">检查项目</h3>
<el-table
:data="currentItems"
@selection-change="handleItemSelection"
row-key="id"
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column prop="spec" label="规格" width="120" />
</el-table>
</div>
<!-- 其他列保持不变 -->
<el-table-column prop="spec" label="规格" width="120" />
<el-table-column prop="deptName" label="申请科室" width="120" />
<el-table-column prop="applyDoctor" label="申请医生" width="120" />
<el-table-column prop="applyTime" label="申请时间" width="180" />
</el-table>
<!-- 右侧已选择区域 & 检查方法 -->
<div class="panel right-panel">
<h3 class="panel-title">已选择项目</h3>
<div class="selected-list">
<div v-for="item in selectedItems" :key="item.id" class="selected-card">
<div class="card-header" @click="toggleExpand(item)">
<el-checkbox
v-model="item.checked"
@change="handleItemCheckChange(item)"
@click.stop
/>
<el-tooltip :content="item.name" placement="top" :show-after="300">
<span class="item-name">{{ item.name }}</span>
</el-tooltip>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<!-- 检查方法/明细区域 (默认收起) -->
<transition name="slide-fade">
<div v-show="item.expanded" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-item">
<el-checkbox
v-model="method.checked"
@change="handleMethodCheckChange(item, method)"
@click.stop
/>
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</transition>
</div>
<div v-if="selectedItems.length === 0" class="empty-tip">暂无选择项目</div>
</div>
</div>
</div>
<div class="actions">
<el-button type="primary" @click="confirmSelection">确认</el-button>
<el-button @click="clearSelection">清空</el-button>
<el-button type="primary" @click="submitRequests" :loading="submitting">提交申请</el-button>
<el-button @click="clearAll">清空</el-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import { fetchCheckRequests, submitCheckRequests } from '@/api/outpatient';
const requestList = ref([]);
const selectedRows = ref([]);
// 模拟分类树数据(实际应从接口获取)
const categoryTree = ref([
{ id: 'cat1', name: '彩超', children: [] },
{ id: 'cat2', name: 'CT', children: [] },
{ id: 'cat3', name: 'MRI', children: [] }
]);
// 表格实例
const table = ref(null);
const currentItems = ref([]);
const selectedItems = reactive([]);
const submitting = ref(false);
// 获取检查申请列表
onMounted(async () => {
const { data } = await fetchCheckRequests();
requestList.value = data;
});
// 行是否可选:防止同一检查项目的冲突自动勾选
const selectableRow = (row) => {
// 同一检查项目itemCode只能选中一条
const alreadySelected = selectedRows.value.some(
(item) => item.itemCode === row.itemCode
);
return !alreadySelected;
// 点击分类加载项目
const handleCategoryClick = async (data) => {
// 实际项目中替换为真实API调用
currentItems.value = [
{ id: `${data.id}_1`, name: '128线排彩超', spec: '常规', methods: [
{ id: 'm1', name: '腹部彩超', checked: false },
{ id: 'm2', name: '心脏彩超', checked: false }
]},
{ id: `${data.id}_2`, name: '高频浅表彩超', spec: '高频', methods: [
{ id: 'm3', name: '甲状腺彩超', checked: false }
]}
];
};
// 处理手动勾选变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection;
// 中间表格勾选联动(仅添加/移除卡片,不自动勾选方法)
const handleItemSelection = (selection) => {
const selectedIds = new Set(selection.map(i => i.id));
// 移除未勾选的
for (let i = selectedItems.length - 1; i >= 0; i--) {
if (!selectedIds.has(selectedItems[i].id)) {
selectedItems.splice(i, 1);
}
}
// 新增勾选的
selection.forEach(item => {
if (!selectedItems.find(s => s.id === item.id)) {
selectedItems.push({
...item,
checked: true,
expanded: false, // 默认收起
methods: item.methods.map(m => ({ ...m, checked: false })) // 方法独立状态
});
}
});
};
// 确认选择
const confirmSelection = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择检查项目');
// 项目勾选状态变更
const handleItemCheckChange = (item) => {
// 仅控制项目本身状态,不联动方法
};
// 方法勾选状态变更(完全独立)
const handleMethodCheckChange = (item, method) => {
// 独立控制,无联动逻辑
};
// 展开/收起明细
const toggleExpand = (item) => {
item.expanded = !item.expanded;
};
// 提交申请
const submitRequests = async () => {
const payload = selectedItems
.filter(i => i.checked)
.map(i => ({
itemCode: i.id,
itemName: i.name,
spec: i.spec,
methods: i.methods.filter(m => m.checked).map(m => ({ methodCode: m.id, methodName: m.name }))
}));
if (payload.length === 0) {
ElMessage.warning('请至少选择一个检查项目');
return;
}
submitting.value = true;
try {
await submitCheckRequests(selectedRows.value);
ElMessage.success('检查申请已提交');
// 提交成功后刷新列表并清空选择
const { data } = await fetchCheckRequests();
requestList.value = data;
clearSelection();
} catch (e) {
ElMessage.error('提交失败,请重试');
await submitCheckRequests(payload);
ElMessage.success('提交成功');
clearAll();
} catch (err) {
ElMessage.error(err.message || '提交失败');
} finally {
submitting.value = false;
}
};
// 清空选择
const clearSelection = () => {
selectedRows.value = [];
if (table.value) {
table.value.clearSelection();
}
// 清空
const clearAll = () => {
selectedItems.length = 0;
currentItems.value = [];
};
</script>
<style scoped>
.check-request {
.check-request-container {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.actions {
margin-top: 20px;
text-align: right;
.layout-wrapper {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.ellipsis {
display: inline-block;
max-width: 180px;
.panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.left-panel { flex: 1; min-width: 200px; }
.middle-panel { flex: 2; min-width: 300px; }
.right-panel { flex: 2; min-width: 300px; }
.panel-title {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
.category-tree { max-height: 500px; overflow-y: auto; }
.selected-list {
max-height: 450px;
overflow-y: auto;
padding-right: 4px;
}
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
background: #fff;
transition: background 0.2s;
}
.card-header:hover { background: #f0f2f5; }
.item-name {
flex: 1;
margin: 0 10px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expand-icon { color: #909399; }
.method-list {
padding: 8px 12px 12px 32px;
background: #f9fafc;
border-top: 1px dashed #ebeef5;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
font-size: 14px;
color: #606266;
}
.method-name { margin-left: 8px; }
.empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.slide-fade-enter-active, .slide-fade-leave-active {
transition: all 0.3s ease;
}
.slide-fade-enter-from, .slide-fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>

View File

@@ -58,49 +58,52 @@ test.describe('HIS 系统回归测试集', () => {
await expect(page).toHaveURL(/.*dashboard.*/);
await page.click('text=住院发退药');
await page.waitForLoadState('networkidle');
// 此处省略具体断言,仅保留结构占位
});
// ================= 新增 Bug #550 回归测试 =================
test('@bug550 @regression 门诊检查申请项目选择交互优化校验', async ({ page }) => {
test('@bug550 @regression 检查申请项目选择交互优化:解耦、卡片展示与层级校验', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="username"]', 'doctor');
await page.fill('input[name="username"]', 'doctor01');
await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard.*/);
await page.click('text=门诊医生站');
await page.click('text=检查申请单');
await page.waitForLoadState('networkidle');
// 1. 验证联动解耦:勾选项目不应自动勾选检查方法
// 1. 验证左侧分类点击加载中间项目
await page.click('text=彩超');
await page.waitForTimeout(500);
const itemCheckbox = page.locator('.exam-item-list .el-checkbox:has-text("128线排") input[type="checkbox"]');
await itemCheckbox.check();
const methodCheckbox = page.locator('.exam-method-list .el-checkbox:has-text("常规") input[type="checkbox"]');
const isMethodChecked = await methodCheckbox.isChecked();
expect(isMethodChecked).toBe(false);
const itemTable = page.locator('.middle-panel .el-table__body-wrapper');
await expect(itemTable).toBeVisible();
// 2. 验证卡片显示:无“套餐”前缀,支持悬停提示完整名称,宽度自适应
const selectedCard = page.locator('.selected-area .el-collapse-item__header').first();
const cardText = await selectedCard.textContent();
expect(cardText).not.toContain('套餐');
expect(cardText).toContain('128线排');
// 2. 勾选项目,验证检查方法未自动勾选(解耦)
const firstItemCheckbox = page.locator('.middle-panel .el-table__body tr').first().locator('input[type="checkbox"]');
await firstItemCheckbox.check();
const titleAttr = await selectedCard.getAttribute('title');
expect(titleAttr).toContain('128线排');
// 验证右侧已选区域出现卡片,且默认收起
const selectedCard = page.locator('.selected-card').first();
await expect(selectedCard).toBeVisible();
const methodList = selectedCard.locator('.method-list');
await expect(methodList).not.toBeVisible(); // 默认收起
// 3. 验证默认状态与层级:明细默认收起,结构为 项目 > 检查方法,无冗余标签
const collapsePanel = page.locator('.selected-area .el-collapse-item');
const isExpanded = await collapsePanel.getAttribute('aria-expanded');
expect(isExpanded).toBe('false');
// 3. 展开卡片,验证检查方法独立勾选
await selectedCard.locator('.card-header').click();
await expect(methodList).toBeVisible();
const firstMethodCheckbox = methodList.locator('.method-item').first().locator('input[type="checkbox"]');
await expect(firstMethodCheckbox).not.toBeChecked(); // 未自动勾选
await firstMethodCheckbox.check();
await expect(firstMethodCheckbox).toBeChecked(); // 手动勾选成功
await collapsePanel.click();
await page.waitForTimeout(300);
const hierarchyText = await page.locator('.selected-area .detail-hierarchy').textContent();
expect(hierarchyText).toMatch(/检查方法/);
expect(hierarchyText).not.toMatch(/项目套餐明细/);
// 4. 验证名称显示完整(无遮挡/无冗余套餐字样)
const itemName = selectedCard.locator('.item-name');
const nameText = await itemName.textContent();
expect(nameText).not.toContain('套餐');
expect(nameText).not.toMatch(/\.{3}$/); // 无省略号遮挡
// 5. 提交校验
await page.click('button:has-text("提交申请")');
await expect(page.locator('.el-message--success')).toContainText('提交成功');
});
});