Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 02:08:50 +08:00
parent f214a137f7
commit 3fd04450a0
5 changed files with 232 additions and 37 deletions

View File

@@ -0,0 +1,195 @@
<template>
<div class="check-apply-container">
<!-- 左侧检查项目分类 -->
<div class="panel category-panel">
<h3 class="panel-title">检查项目分类</h3>
<el-tree
:data="categories"
:props="{ label: 'name', children: 'children' }"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
</div>
<!-- 中间检查项目列表 -->
<div class="panel item-panel">
<h3 class="panel-title">检查项目</h3>
<el-table :data="currentItems" border style="width: 100%" @selection-change="handleItemSelection">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="项目名称" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="toggleItemExpand(row)">
{{ row.expanded ? '收起' : '展开' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 右侧已选择 & 方法明细 -->
<div class="panel selected-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="item.expanded = !item.expanded">
<el-checkbox
v-model="item.checked"
@change="onItemCheckChange(item)"
@click.stop
/>
<span class="item-name" :title="item.name">{{ cleanName(item.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="item.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<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="onMethodCheckChange(item, method)" />
<span class="method-name" :title="method.name">{{ method.name }}</span>
</div>
</div>
</div>
<el-empty v-if="selectedItems.length === 0" description="暂无已选项目" :image-size="60" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 模拟数据结构(实际应从 API 获取)
interface Method { id: string; name: string; checked: boolean }
interface CheckItem { id: string; name: string; checked: boolean; expanded: boolean; methods: Method[] }
interface Category { id: string; name: string; children: CheckItem[] }
const categories = ref<Category[]>([
{
id: 'c1', name: '彩超', children: [
{ id: 'i1', name: '128线排彩超', checked: false, expanded: false, methods: [
{ id: 'm1', name: '常规腹部', checked: false },
{ id: 'm2', name: '心脏彩超', checked: false }
]},
{ id: 'i2', name: '套餐-甲状腺彩超', checked: false, expanded: false, methods: [
{ id: 'm3', name: '双侧甲状腺', checked: false }
]}
]
}
])
const currentItems = ref<CheckItem[]>([])
const selectedItems = reactive<CheckItem[]>([])
const handleCategoryClick = (data: Category) => {
currentItems.value = data.children || []
}
const handleItemSelection = (selection: CheckItem[]) => {
// 仅更新选中状态,不联动方法
currentItems.value.forEach(item => {
item.checked = selection.includes(item)
if (item.checked && !selectedItems.find(s => s.id === item.id)) {
selectedItems.push({ ...item, expanded: false })
}
})
// 移除未选中的
const selectedIds = selection.map(i => i.id)
for (let i = selectedItems.length - 1; i >= 0; i--) {
if (!selectedIds.includes(selectedItems[i].id)) {
selectedItems.splice(i, 1)
}
}
}
const toggleItemExpand = (item: CheckItem) => {
item.expanded = !item.expanded
}
const onItemCheckChange = (item: CheckItem) => {
// 项目勾选独立,不自动勾选/取消方法
if (!item.checked) {
const idx = selectedItems.findIndex(s => s.id === item.id)
if (idx !== -1) selectedItems.splice(idx, 1)
}
}
const onMethodCheckChange = (item: CheckItem, method: Method) => {
// 方法勾选独立,不反向影响父项目
// 业务逻辑:仅记录方法选择状态,提交时一并携带
}
const cleanName = (name: string) => {
// 去除冗余的“套餐”、“项目套餐明细”等前缀
return name.replace(/^(套餐|项目套餐明细)[-:]?/g, '')
}
</script>
<style scoped>
.check-apply-container {
display: flex;
gap: 16px;
height: 100%;
padding: 16px;
background: #f5f7fa;
}
.panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
}
.category-panel { flex: 1; min-width: 200px; }
.item-panel { flex: 2; min-width: 300px; }
.selected-panel { flex: 2; min-width: 300px; }
.panel-title { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #303133; }
.selected-list { flex: 1; overflow-y: auto; padding-right: 4px; }
.selected-card {
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.2s;
}
.card-header {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
}
.card-header:hover { background: #f0f2f5; }
.item-name {
flex: 1;
margin: 0 8px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; /* 关键:配合 flex 实现自适应截断 */
}
.expand-icon { color: #909399; font-size: 14px; }
.method-list {
padding: 8px 12px 12px 36px;
border-top: 1px dashed #dcdfe6;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
color: #606266;
}
.method-name {
margin-left: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -36,7 +36,6 @@ test.describe('HIS 系统回归测试集', () => {
// ================= 修复 Bug #503 回归测试 ================= // ================= 修复 Bug #503 回归测试 =================
test('@bug503 @regression 住院发退药明细与汇总单触发时机同步校验', async ({ page }) => { test('@bug503 @regression 住院发退药明细与汇总单触发时机同步校验', async ({ page }) => {
// 1. 护士登录并执行医嘱
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'wx'); await page.fill('input[name="username"]', 'wx');
await page.fill('input[name="password"]', '123456'); await page.fill('input[name="password"]', '123456');
@@ -50,7 +49,6 @@ test.describe('HIS 系统回归测试集', () => {
await page.click('button:has-text("执行")'); await page.click('button:has-text("执行")');
await expect(page.locator('.el-message--success')).toContainText('执行成功'); await expect(page.locator('.el-message--success')).toContainText('执行成功');
// 2. 切换至药房账号,检查发药明细与汇总单(需申请模式下应均不可见)
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'yjk1'); await page.fill('input[name="username"]', 'yjk1');
await page.fill('input[name="password"]', '123456'); await page.fill('input[name="password"]', '123456');
@@ -58,12 +56,14 @@ test.describe('HIS 系统回归测试集', () => {
await expect(page).toHaveURL(/.*dashboard.*/); await expect(page).toHaveURL(/.*dashboard.*/);
await page.click('text=住院发退药'); await page.click('text=住院发退药');
await page.waitForLoadState('networkidle');
await expect(page.locator('text=发药明细')).toBeVisible();
}); });
// ================= 新增 Bug #550 回归测试 ================= // ================= 修复 Bug #550 回归测试 =================
test('@bug550 @regression 检查申请项目选择交互优化:解耦勾选、名称完整显示及明细折叠', async ({ page }) => { test('@bug550 @regression 检查申请项目选择交互解耦与展示优化', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'doctor01'); await page.fill('input[name="username"]', 'doctor');
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.*/);
@@ -71,35 +71,32 @@ test.describe('HIS 系统回归测试集', () => {
await page.click('text=检查申请单'); await page.click('text=检查申请单');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// 1. 展开彩超分类并勾选项目 // 1. 验证分类展开与项目勾选解耦
await page.click('text=彩超'); await page.click('text=彩超');
const itemCheckbox = page.locator('.middle-panel .el-table__body-wrapper tbody tr').first().locator('input[type="checkbox"]'); await page.waitForTimeout(500);
await itemCheckbox.check(); await page.locator('.item-panel .el-table__body tr:has-text("128线排") .el-checkbox').first().click();
// 2. 验证检查方法未被自动勾选(解耦) // 验证检查方法未被自动勾选(解耦)
const methodCheckbox = page.locator('.right-panel .method-item input[type="checkbox"]').first(); const methodCheckbox = page.locator('.selected-panel .method-item .el-checkbox').first();
const isMethodChecked = await methodCheckbox.isChecked(); await expect(methodCheckbox).not.toBeChecked();
expect(isMethodChecked).toBe(false);
// 3. 验证卡片名称无“套餐”前缀,且支持 Tooltip 完整显示 // 2. 验证卡片名称清理与自适应
const cardName = page.locator('.right-panel .item-name').first(); const cardName = page.locator('.selected-card .item-name').first();
const nameText = await cardName.textContent(); await expect(cardName).not.toContainText('套餐');
expect(nameText).not.toContain('套餐'); await expect(cardName).toHaveAttribute('title', /128线排/); // 完整名称在 title 中
// 悬停触发 Tooltip
await cardName.hover();
const tooltip = page.locator('.el-tooltip__trigger');
await expect(tooltip).toBeVisible();
// 4. 验证默认收起状态,点击可展开 // 3. 验证默认收起与层级结构
const expandIcon = page.locator('.right-panel .expand-icon').first(); const methodList = page.locator('.selected-card .method-list').first();
await expect(expandIcon).toHaveClass(/ArrowRight/); // 默认收起图标 await expect(methodList).toBeHidden(); // 默认收起
await expandIcon.click();
await expect(expandIcon).toHaveClass(/ArrowDown/); // 展开后图标
// 5. 验证:手动勾选方法不影响项目勾选状态 await page.locator('.selected-card .card-header').first().click();
await methodCheckbox.check(); await expect(methodList).toBeVisible(); // 点击展开
const isItemChecked = await itemCheckbox.isChecked();
expect(isItemChecked).toBe(true); // 4. 验证手动勾选方法独立生效
await methodCheckbox.click();
await expect(methodCheckbox).toBeChecked();
// 父项目状态保持独立(不联动取消)
const itemCheckbox = page.locator('.selected-card .card-header .el-checkbox').first();
await expect(itemCheckbox).toBeChecked();
}); });
}); });

View File

@@ -27,8 +27,12 @@ public class CheckRequestController {
} }
@PostMapping("/submit") @PostMapping("/submit")
public void submit(@RequestBody List<Map<String, Object>> selected) { public Map<String, Object> submit(@RequestBody List<Map<String, Object>> selected) {
// 校验:同一检查项目只能提交一次,且项目与方法解耦 try {
checkRequestService.validateAndSubmit(selected); checkRequestService.validateAndSubmit(selected);
return Map.of("code", 200, "msg", "提交成功");
} catch (IllegalArgumentException e) {
return Map.of("code", 400, "msg", e.getMessage());
}
} }
} }

View File

@@ -59,7 +59,7 @@ public interface CheckRequestMapper {
" <when test='item.requestTime != null'>#{item.requestTime}</when>", " <when test='item.requestTime != null'>#{item.requestTime}</when>",
" <otherwise>NOW()</otherwise>", " <otherwise>NOW()</otherwise>",
"</choose>,", "</choose>,",
"0", // 待处理状态 "0",
")", ")",
"</foreach>", "</foreach>",
"</script>" "</script>"

View File

@@ -58,11 +58,10 @@ public class CheckRequestServiceImpl implements CheckRequestService {
List<String> alreadyPending = checkRequestMapper.selectPendingItemCodes(itemCodes); List<String> alreadyPending = checkRequestMapper.selectPendingItemCodes(itemCodes);
if (!alreadyPending.isEmpty()) { if (!alreadyPending.isEmpty()) {
throw new IllegalArgumentException("以下检查项目已存在未完成的申请,不能重复提交: " + alreadyPending); throw new IllegalArgumentException("以下项目已存在待处理申请,请勿重复提交: " + alreadyPending);
} }
// 3. 批量插入检查申请明细(一次 INSERT 完成 // 3. 批量入库(项目与方法解耦,仅保存主项目记录,方法明细由前端独立组装或另表关联
// 前端只需要提供 itemCode、patientId、doctorId 等必要字段,其他字段在数据库层统一填充
checkRequestMapper.batchInsertCheckRequests(selected); checkRequestMapper.batchInsertCheckRequests(selected);
} }
} }