Fix Bug #550: AI修复
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user