Fix Bug #550: fallback修复
This commit is contained in:
@@ -54,130 +54,174 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
// 模拟数据结构 (实际应从API获取)
|
/**
|
||||||
interface CategoryNode { id: string; name: string; children?: ExamItem[] }
|
* 说明
|
||||||
interface ExamItem { id: string; name: string; methods?: ExamMethod[] }
|
* -----
|
||||||
interface ExamMethod { id: string; name: string }
|
* 本组件原有的交互存在以下问题(Bug #550):
|
||||||
|
* 1. **自动勾选冲突**:在“已选择”列表中手动勾选检查方法后,若再次在左侧列表中勾选同一检查项目,会出现重复的分组,导致方法选中状态错乱。
|
||||||
|
* 2. **名称遮挡**:检查项目名称过长时,折叠标题会被截断,用户看不到完整名称。
|
||||||
|
* 3. **明细耦合**:已选择的分组与检查项目列表之间没有解耦,导致取消勾选时未同步移除对应的分组。
|
||||||
|
*
|
||||||
|
* 为了解决上述问题,做了以下改动:
|
||||||
|
* - 使用 `Map`(`selectedGroupsMap`)在内部维护已选择的分组,确保同一检查项目只会出现唯一的分组实例,避免重复。
|
||||||
|
* - 在折叠标题上使用 `el-tooltip` 并去除 “套餐” 前缀,保证完整名称可见。
|
||||||
|
* - 通过 `watch(selectedItemIds)` 实时同步已选择的检查项目与分组列表,实现双向解耦。
|
||||||
|
* - 默认折叠全部关闭(`activeCollapseNames` 初始为空),用户展开时才看到详情。
|
||||||
|
* - 为每个分组的 `selectedMethodIds` 使用 `Set`,防止方法重复勾选。
|
||||||
|
*/
|
||||||
|
|
||||||
const categoryTree = ref<CategoryNode[]>([
|
/* --------------------------- 组件状态 --------------------------- */
|
||||||
{
|
const categoryTree = ref<Array<any>>([]); // 由父组件或接口注入
|
||||||
id: 'cat_1', name: '彩超', children: [
|
const allItems = ref<Array<any>>([]); // 所有检查项目(包含分类信息),同样由父组件或接口注入
|
||||||
{ id: 'item_1', name: '128线排套餐', methods: [{ id: 'm1', name: '常规' }, { id: 'm2', name: '三维' }] },
|
|
||||||
{ id: 'item_2', name: '腹部彩超', methods: [{ id: 'm3', name: '常规' }] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const currentItems = ref<ExamItem[]>([])
|
// 左侧树选中的分类 ID
|
||||||
const selectedItemIds = ref<string[]>([])
|
const selectedCategoryId = ref<number | null>(null);
|
||||||
// 修复3:默认空数组,实现面板默认收起状态
|
|
||||||
const activeCollapseNames = ref<string[]>([])
|
|
||||||
|
|
||||||
// 已选数据映射:独立维护项目与方法状态
|
// 右侧检查项目复选框绑定的 ID 集合(el-checkbox-group 要求是数组)
|
||||||
const selectedDataMap = ref<Map<string, { id: string; name: string; methods: ExamMethod[]; selectedMethodIds: string[] }>>(new Map())
|
const selectedItemIds = ref<Array<number>>([]);
|
||||||
|
|
||||||
// 计算属性:渲染已选择分组
|
// 已选择的分组(用于渲染折叠面板),结构如下:
|
||||||
const selectedGroups = computed(() => {
|
// {
|
||||||
return Array.from(selectedDataMap.value.values()).map(item => ({
|
// itemId: number,
|
||||||
itemId: item.id,
|
// itemName: string,
|
||||||
// 修复2:清理冗余“套餐”字样
|
// methods: Array<{id:number, name:string}>,
|
||||||
itemName: item.name.replace(/套餐/g, ''),
|
// selectedMethodIds: Set<number>
|
||||||
methods: item.methods || [],
|
// }
|
||||||
selectedMethodIds: item.selectedMethodIds || []
|
const selectedGroups = ref<Array<any>>([]);
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分类点击
|
// 用 Map 快速定位已存在的分组,键为 itemId,值为分组对象
|
||||||
const handleCategorySelect = (node: CategoryNode) => {
|
const selectedGroupsMap = new Map<number, any>();
|
||||||
currentItems.value = node.children || []
|
|
||||||
|
// 控制折叠面板展开的项(默认全部收起)
|
||||||
|
const activeCollapseNames = ref<Array<number>>([]);
|
||||||
|
|
||||||
|
/* --------------------------- 计算属性 --------------------------- */
|
||||||
|
// 根据选中的分类过滤检查项目列表
|
||||||
|
const currentItems = computed(() => {
|
||||||
|
if (selectedCategoryId.value === null) return allItems.value;
|
||||||
|
return allItems.value.filter(item => item.categoryId === selectedCategoryId.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* --------------------------- 方法 --------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类树节点点击回调
|
||||||
|
*/
|
||||||
|
function handleCategorySelect(node: any) {
|
||||||
|
selectedCategoryId.value = node.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复1:项目勾选与检查方法解耦
|
/**
|
||||||
const onItemChange = (ids: string[]) => {
|
* 检查项目复选框变化时触发
|
||||||
const added = ids.filter(id => !selectedItemIds.value.includes(id))
|
* - 同步 `selectedGroups` 与 `selectedItemIds`
|
||||||
const removed = selectedItemIds.value.filter(id => !ids.includes(id))
|
* - 防止同一项目出现多次分组
|
||||||
|
*/
|
||||||
|
function onItemChange() {
|
||||||
|
// 1️⃣ 先把当前已选的 ID 转成 Set,便于快速判断
|
||||||
|
const newSelectedSet = new Set(selectedItemIds.value);
|
||||||
|
|
||||||
// 处理新增项目
|
// 2️⃣ 移除已经不在 newSelectedSet 中的分组
|
||||||
added.forEach(id => {
|
for (const [itemId, group] of selectedGroupsMap.entries()) {
|
||||||
const item = currentItems.value.find(i => i.id === id)
|
if (!newSelectedSet.has(itemId)) {
|
||||||
if (item) {
|
selectedGroupsMap.delete(itemId);
|
||||||
selectedDataMap.value.set(id, {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
methods: item.methods || [],
|
|
||||||
// 关键修复:不自动勾选关联方法,保持独立手动选择
|
|
||||||
selectedMethodIds: []
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// 处理移除项目
|
|
||||||
removed.forEach(id => {
|
|
||||||
selectedDataMap.value.delete(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
selectedItemIds.value = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// 方法独立勾选变更
|
|
||||||
const onMethodChange = (group: { itemId: string; selectedMethodIds: string[] }) => {
|
|
||||||
const item = selectedDataMap.value.get(group.itemId)
|
|
||||||
if (item) {
|
|
||||||
item.selectedMethodIds = group.selectedMethodIds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
.map(id => selectedGroupsMap.get(id))
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查方法复选框变化时触发
|
||||||
|
* @param group 当前分组对象
|
||||||
|
*/
|
||||||
|
function onMethodChange(group: any) {
|
||||||
|
// `group.selectedMethodIds` 仍然是数组(el-checkbox-group 的 v-model),
|
||||||
|
// 这里把它转成 Set,保持内部数据结构统一。
|
||||||
|
group.selectedMethodIds = new Set(group.selectedMethodIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- 监听 --------------------------- */
|
||||||
|
// 当外部可能直接修改 `selectedItemIds`(例如表单回填)时,同步分组数据
|
||||||
|
watch(selectedItemIds, () => {
|
||||||
|
onItemChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* --------------------------- 初始化 --------------------------- */
|
||||||
|
// 示例数据(实际项目请通过接口注入)
|
||||||
|
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: 12px;
|
padding: 16px;
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.selector-layout {
|
.selector-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
height: 480px;
|
|
||||||
}
|
}
|
||||||
.panel {
|
.panel {
|
||||||
flex: 1;
|
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
overflow-y: auto;
|
background-color: #fafafa;
|
||||||
background: #fafafa;
|
|
||||||
}
|
}
|
||||||
.panel-title {
|
.category-panel,
|
||||||
margin: 0 0 10px 0;
|
.item-panel {
|
||||||
font-size: 14px;
|
width: 30%;
|
||||||
font-weight: 600;
|
}
|
||||||
color: #303133;
|
.selected-panel {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.item-list {
|
.item-list {
|
||||||
display: flex;
|
max-height: 400px;
|
||||||
flex-direction: column;
|
overflow-y: auto;
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.item-checkbox {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.selected-collapse {
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
.group-title {
|
.group-title {
|
||||||
max-width: 140px;
|
display: inline-block;
|
||||||
|
max-width: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
}
|
||||||
.method-section {
|
.method-section {
|
||||||
padding: 8px 0 8px 16px;
|
margin-top: 8px;
|
||||||
border-left: 2px solid #e4e7ed;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user