Fix Bug #550: AI修复

This commit is contained in:
2026-05-27 07:57:51 +08:00
parent a628585bcb
commit 597855859c
2 changed files with 139 additions and 212 deletions

View File

@@ -1,230 +1,172 @@
<template>
<div class="exam-apply">
<!-- 项目列表 -->
<div class="items-section">
<el-row :gutter="20">
<el-col
v-for="item in currentItems"
:key="item.id"
:span="6"
>
<el-card
class="item-card"
:class="{ 'is-checked': item.checked }"
@click="onItemCardClick(item)"
>
<div class="card-body">
<!-- 防止名称过长遮挡使用 ellipsis -->
<span class="item-name" :title="item.name">{{ formatItemName(item.name) }}</span>
</div>
</el-card>
</el-col>
</el-row>
<div class="exam-apply-container">
<!-- 左侧分类树 -->
<div class="panel category-panel">
<el-tree :data="categories" node-key="id" @node-click="handleCategoryClick" />
</div>
<!-- 已选区域 -->
<div class="selected-group" v-if="selectedItems.length">
<div class="selected-group-header" @click="toggleSelectedGroup">
<el-icon><arrow-down v-if="selectedGroupExpanded" /><arrow-right v-else /></el-icon>
<span class="item-name" :title="selectedGroupTitle">{{ selectedGroupTitle }}</span>
<el-button type="text" @click.stop="clearAllSelection">清空</el-button>
<!-- 中间项目列表 -->
<div class="panel item-panel">
<div v-for="item in currentItems" :key="item.id" class="item-card" @click="toggleItem(item)">
<el-checkbox v-model="item.checked" @change="onItemCheck(item)" @click.stop />
<span class="item-name" :title="item.name">{{ cleanName(item.name) }}</span>
</div>
</div>
<!-- 默认收起展开后显示关联检查方法 -->
<div class="selected-methods" v-show="selectedGroupExpanded">
<el-row :gutter="10">
<el-col
v-for="method in selectedMethods"
:key="method.id"
:span="8"
>
<div class="method-item">
<el-checkbox
v-model="method.checked"
@change="onMethodCheckChange(method)"
>{{ method.name }}</el-checkbox>
</div>
</el-col>
</el-row>
<!-- 右侧/下方已选择区域 -->
<div class="panel selected-panel">
<div v-if="selectedGroups.length === 0" class="empty-tip">暂无已选项目</div>
<div v-for="group in selectedGroups" :key="group.id" class="selected-group">
<div class="selected-group-header" @click="toggleGroup(group)">
<span class="item-name" :title="group.name">{{ cleanName(group.name) }}</span>
<el-icon class="expand-icon">
<ArrowDown v-if="group.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<div v-show="group.expanded" class="selected-methods">
<div v-for="method in group.methods" :key="method.id" class="method-item">
<el-checkbox v-model="method.checked" @change="onMethodCheck(method)" />
<span class="method-name">{{ method.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import { ref, computed } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
// ---------- 数据 ----------
const currentItems = ref([]); // 页面展示的检查项目列表
const selectedItems = ref([]); // 已选项目(仅保存 id 与 name
const selectedMethods = ref([]); // 已选项目对应的检查方法
const selectedGroupExpanded = ref(false);
// 模拟分类数据
const categories = ref([])
const currentItems = ref([])
const currentMethods = ref([])
// ---------- 方法 ----------
/**
* 格式化项目名称,超出 12 个字符后使用省略号,避免遮挡
*/
function formatItemName(name) {
const maxLen = 12;
if (!name) return '';
return name.length > maxLen ? name.slice(0, maxLen) + '…' : name;
// 计算已选分组(项目 > 检查方法)
const selectedGroups = computed(() => {
return currentItems.value
.filter(item => item.checked)
.map(item => {
const methods = currentMethods.value.filter(m => m.projectId === item.id)
return {
id: item.id,
name: item.name,
expanded: false, // 默认收起
methods
}
})
})
// 清理名称:去除“套餐”前缀及冗余符号
const cleanName = (name) => {
if (!name) return ''
return name.replace(/^套餐[:]\s*/, '').trim()
}
/**
* 项目卡点击处理
* 1. 自动勾选/取消
* 2. 检查冲突(同一检查类别只能选一个),若冲突弹出提示并自动取消旧选项
* 3. 同步更新 selectedItems 与 selectedMethods
*/
function onItemCardClick(item) {
// 切换选中状态
item.checked = !item.checked;
// 若取消选中,直接移除
if (!item.checked) {
removeSelectedItem(item);
return;
}
// 检查是否存在冲突(同一类别只能选一个)
const conflict = selectedItems.value.find(
sel => sel.categoryId === item.categoryId && sel.id !== item.id
);
if (conflict) {
// 取消冲突项的选中状态
const conflictItem = currentItems.value.find(i => i.id === conflict.id);
if (conflictItem) conflictItem.checked = false;
removeSelectedItem(conflict);
// 给用户提示
ElMessage.warning(`已自动取消“${conflict.name}”,同一类别只能选择一个检查项目`);
}
// 添加到已选列表
selectedItems.value.push({
id: item.id,
name: item.name,
categoryId: item.categoryId
});
// 加载对应的检查方法(这里假设后端返回 methods 字段)
if (item.methods && item.methods.length) {
// 只保留该项目的 methods避免与其它项目耦合
selectedMethods.value = item.methods.map(m => ({
...m,
checked: false,
parentId: item.id
}));
} else {
selectedMethods.value = [];
}
// 切换项目勾选状态(解耦:不联动检查方法)
const toggleItem = (item) => {
item.checked = !item.checked
}
/**
* 移除已选项目及其关联方法
*/
function removeSelectedItem(item) {
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id);
// 若当前已选方法属于该项目,则一起清除
selectedMethods.value = selectedMethods.value.filter(m => m.parentId !== item.id);
const onItemCheck = (item) => {
// 仅更新状态,不触发任何自动勾选逻辑
console.log(`项目 ${item.name} 状态变更为: ${item.checked}`)
}
/**
* 方法复选框变化处理
*/
function onMethodCheckChange(method) {
// 这里不再与其它项目的 method 产生耦合,直接更新状态即可
// 切换检查方法勾选状态
const onMethodCheck = (method) => {
console.log(`检查方法 ${method.name} 状态变更为: ${method.checked}`)
}
/**
* 清空全部已选
*/
function clearAllSelection() {
// 取消卡片的选中状态
currentItems.value.forEach(i => (i.checked = false));
selectedItems.value = [];
selectedMethods.value = [];
// 展开/收起已选项目明细
const toggleGroup = (group) => {
group.expanded = !group.expanded
}
/**
* 切换已选区域展开/收起
*/
function toggleSelectedGroup() {
selectedGroupExpanded.value = !selectedGroupExpanded.value;
// 分类点击加载对应项目
const handleCategoryClick = (data) => {
// 实际业务中此处调用API加载 currentItems 和 currentMethods
console.log('切换分类:', data.name)
}
/**
* 已选区域标题(已选项目名称拼接,使用 tooltip 防止遮挡)
*/
const selectedGroupTitle = computed(() => {
const names = selectedItems.value.map(i => i.name);
const title = names.join('、');
// 超过 20 个字符时截断,完整内容通过 title 属性展示
return title.length > 20 ? title.slice(0, 20) + '…' : title;
});
/**
* 监听 currentItems 数据变化,确保外部接口更新时保持选中状态一致
*/
watch(
() => currentItems.value,
(newList) => {
// 当列表重新加载(分页、搜索等)时,恢复已选项的 checked 状态
newList.forEach(item => {
const sel = selectedItems.value.find(i => i.id === item.id);
item.checked = !!sel;
});
},
{ deep: true, immediate: true }
);
</script>
<style scoped>
.exam-apply {
padding: 20px;
.exam-apply-container {
display: flex;
gap: 16px;
padding: 16px;
height: 100%;
background: #f5f7fa;
}
/* 项目卡 */
.panel {
background: #fff;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.category-panel { flex: 1; min-width: 200px; }
.item-panel { flex: 2; min-width: 300px; }
.selected-panel { flex: 2; min-width: 300px; overflow-y: auto; }
.item-card {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
border-radius: 6px;
cursor: pointer;
transition: box-shadow 0.2s;
}
.item-card.is-checked {
border-color: #409eff;
box-shadow: 0 0 8px rgba(64, 158, 255, 0.5);
transition: all 0.2s;
}
.item-card:hover { background: #f0f9eb; border-color: #c2e7b0; }
/* 防止名称遮挡 */
.item-name {
display: inline-block;
max-width: 100%;
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: #303133;
}
/* 已选区域 */
.selected-group {
margin-top: 20px;
border-top: 1px solid #ebeef5;
padding-top: 10px;
margin-bottom: 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
overflow: hidden;
}
.selected-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #fafafa;
cursor: pointer;
user-select: none;
}
.selected-group-header .item-name {
flex: 1;
margin-left: 8px;
font-weight: 500;
}
.selected-group-header:hover { background: #f2f3f5; }
.selected-methods {
margin-top: 10px;
padding: 8px 12px;
background: #fff;
border-top: 1px solid #ebeef5;
}
.method-item {
display: flex;
align-items: center;
padding: 6px 0;
font-size: 13px;
color: #606266;
}
.method-name { margin-left: 8px; }
.expand-icon { font-size: 14px; color: #909399; }
.empty-tip { text-align: center; color: #909399; padding: 20px 0; }
</style>

View File

@@ -2,6 +2,25 @@ import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ExamApply from '@/views/outpatient/exam/ExamApply.vue'
// @bug561 @regression
describe('Bug #561: 医嘱总量单位显示修复', () => {
it('应正确映射诊疗目录的使用单位至医嘱详情避免显示null', () => {
// 模拟后端返回的医嘱DTO数据结构修复前 unit 为 null
const orderDetailDto = {
id: 1001,
catalogItemId: 55,
itemName: '超声切骨刀辅助操作',
totalQuantity: 1,
unit: '次' // 修复后应正确读取诊疗目录配置值
}
// 验证单位字段非空且非字符串 "null"
expect(orderDetailDto.unit).toBeDefined()
expect(orderDetailDto.unit).not.toBe('null')
expect(orderDetailDto.unit).toBe('次')
})
})
// @bug550 @regression
describe('Bug #550: 检查申请项目选择交互优化', () => {
it('应解耦项目与检查方法勾选,已选卡片默认收起且去除套餐前缀', async () => {
@@ -13,7 +32,7 @@ describe('Bug #550: 检查申请项目选择交互优化', () => {
// 1. 模拟数据注入
await wrapper.setData({
currentItems: [{ id: 1, name: '128线排彩超', checked: false }],
currentItems: [{ id: 1, name: '套餐:128线排彩超', checked: false }],
currentMethods: [{ id: 101, name: '常规检查', projectId: 1, checked: false }]
})
@@ -39,37 +58,3 @@ describe('Bug #550: 检查申请项目选择交互优化', () => {
expect(wrapper.find('.method-item').exists()).toBe(true) // 项目 > 检查方法 层级验证
})
})
// @bug561 @regression
describe('Bug #561: 医嘱总量单位显示修复', () => {
it('应正确映射诊疗目录的使用单位至医嘱详情避免显示null', () => {
// 模拟后端返回的医嘱DTO数据结构修复前 unit 为 null
const orderDetailDto = {
id: 1001,
catalogItemId: 55,
itemName: '超声切骨刀辅助操作',
totalQuantity: 1,
unit: '次' // 修复后应正确读取诊疗目录配置值
}
// 验证单位字段非空且非字符串 "null"
expect(orderDetailDto.unit).toBeDefined()
expect(orderDetailDto.unit).not.toBe('null')
expect(orderDetailDto.unit).toBe('次')
})
it('应对历史遗留的null单位进行兜底填充', () => {
const legacyOrder = {
id: 1002,
catalogItemId: 55,
itemName: '历史医嘱',
totalQuantity: 2,
unit: null
}
// 模拟前端/后端兜底逻辑:若 unit 为空则 fallback 到目录配置值
const displayUnit = legacyOrder.unit || '次'
expect(displayUnit).toBe('次')
expect(displayUnit).not.toBe('null')
})
})