Fix Bug #550: fallback修复

This commit is contained in:
2026-05-27 05:32:24 +08:00
parent f7110c6b55
commit 197ea63ea4

View File

@@ -1,216 +1,227 @@
<template>
<div class="exam-apply-container">
<!-- 左侧检查项目分类 -->
<div class="category-panel">
<el-tree
:data="categories"
node-key="id"
highlight-current
@node-click="handleCategoryClick"
/>
<div class="exam-apply">
<!-- 检查项目列表 -->
<el-tree
ref="itemTree"
:data="itemTreeData"
node-key="id"
show-checkbox
:default-expand-all="false"
:props="itemTreeProps"
@check-change="onItemCheckChange"
/>
<!-- 已选项目卡片 -->
<div class="selected-card" :title="selectedCardTitle">
<el-tag v-for="group in selectedGroups" :key="group.id" closable @close="removeGroup(group)">
{{ group.displayName }}
</el-tag>
</div>
<!-- 中间检查项目列表 -->
<div class="project-panel">
<h3>检查项目</h3>
<el-checkbox-group v-model="selectedProjectIds" class="project-list">
<el-checkbox
v-for="item in currentProjects"
:key="item.id"
:label="item.id"
class="item-checkbox"
:data-id="item.id"
>
{{ cleanName(item.name) }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 选中明细默认收起 -->
<el-collapse v-model="activePanels" class="selected-details">
<el-collapse-item
v-for="group in selectedGroups"
:key="group.id"
:name="group.id"
>
<template #title>
<span class="group-header">{{ group.displayName }}</span>
</template>
<!-- 右侧检查方法列表 -->
<div class="method-panel">
<h3>检查方法</h3>
<el-checkbox-group v-model="selectedMethodIds" class="method-list">
<el-checkbox
v-for="method in currentMethods"
:key="method.id"
:label="method.id"
class="method-checkbox"
:data-id="method.id"
>
{{ method.name }}
</el-checkbox>
</el-checkbox-group>
</div>
<!-- 底部已选择区域结构化展示 -->
<div class="selected-area">
<h3>已选择</h3>
<div class="selected-list">
<div v-for="group in selectedGroups" :key="group.projectId" class="selected-group">
<div class="group-header" @click="toggleGroup(group.projectId)">
<el-tooltip :content="group.projectName" placement="top" :show-after="300">
<span class="selected-card" :title="group.projectName">{{ group.projectName }}</span>
</el-tooltip>
<el-icon class="toggle-icon">
<ArrowDown v-if="group.expanded" />
<ArrowRight v-else />
</el-icon>
</div>
<transition name="slide-fade">
<div v-show="group.expanded" class="selected-details">
<div v-for="method in group.methods" :key="method.id" class="method-item">
{{ method.name }}
</div>
</div>
</transition>
</div>
</div>
</div>
<ul class="method-list">
<li
v-for="method in group.methods"
:key="method.id"
class="method-item"
>
<el-checkbox
:label="method.id"
v-model="method.checked"
@change="onMethodChange(group, method)"
>
{{ method.name }}
</el-checkbox>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import type { TreeNode } from 'element-plus/lib/components/tree/src/tree.type'
// 模拟数据源(实际应从 API 获取)
const categories = ref([
{ id: 'cat_1', label: '彩超', children: [] }
])
/**
* 数据结构约定
* - 项目 (Item) 与 检查方法 (Method) 为两层树结构
* - 项目节点 id 形如 "item_128"
* - 方法节点 id 形如 "method_001"
* - 为避免 UI 与业务耦合,选中项目仅记录项目本身,方法的选中状态独立维护
*/
const projectData = ref([
{ id: 'item_128', name: '128线排彩超套餐', categoryId: 'cat_1', methods: ['method_default', 'method_doppler'] },
{ id: 'item_129', name: '常规彩超', categoryId: 'cat_1', methods: ['method_default'] }
])
const methodData = ref([
{ id: 'method_default', name: '常规检查' },
{ id: 'method_doppler', name: '多普勒血流' }
])
// 状态管理
const currentCategoryId = ref('cat_1')
const selectedProjectIds = ref([])
const selectedMethodIds = ref([])
const expandedGroups = ref(new Set())
// 计算属性
const currentProjects = computed(() => projectData.value.filter(p => p.categoryId === currentCategoryId.value))
const currentMethods = computed(() => methodData.value)
// 清理名称:去除“套餐”等冗余前缀
const cleanName = (name) => name.replace(/套餐/g, '')
// 构建已选层级结构:项目 > 检查方法
const selectedGroups = computed(() => {
return selectedProjectIds.value.map(pid => {
const project = projectData.value.find(p => p.id === pid)
if (!project) return null
// 仅展示已勾选的方法,若无勾选则展示默认关联方法(保持独立解耦,不自动勾选)
const methods = project.methods
.filter(mid => selectedMethodIds.value.includes(mid))
.map(mid => methodData.value.find(m => m.id === mid))
.filter(Boolean)
return {
projectId: project.id,
projectName: cleanName(project.name),
methods,
expanded: expandedGroups.value.has(project.id)
}
}).filter(Boolean)
})
// 交互逻辑
const handleCategoryClick = (node) => {
currentCategoryId.value = node.id
interface Method {
id: string
name: string
checked: boolean
}
const toggleGroup = (projectId) => {
if (expandedGroups.value.has(projectId)) {
expandedGroups.value.delete(projectId)
interface ItemGroup {
id: string
name: string
displayName: string // 用于卡片展示,已去除 “套餐” 前缀
methods: Method[]
}
/* ---------- Mock Data (实际请改为接口获取) ---------- */
const rawTree = ref<TreeNode[]>([
{
id: 'item_128',
label: '套餐-128线排',
children: [
{ id: 'method_default', label: '默认方法' },
{ id: 'method_advanced', label: '高级方法' }
]
},
{
id: 'item_256',
label: '套餐-256线排',
children: [{ id: 'method_default', label: '默认方法' }]
}
])
/* ------------------------------------------------------ */
/* 树属性配置 */
const itemTreeProps = {
children: 'children',
label: 'label'
}
/* 将原始树转换为内部可操作结构 */
const itemTreeData = computed(() => {
return rawTree.value.map(item => ({
id: item.id,
label: item.label.replace(/^套餐-/, ''), // 去除 “套餐-” 前缀
children: (item.children ?? []).map(m => ({
id: m.id,
label: m.label
}))
}))
})
/* 已选项目(去耦合) */
const selectedGroups = ref<ItemGroup[]>([])
/* 选中项目卡片的 title完整名称提示 */
const selectedCardTitle = computed(() => {
return selectedGroups.value.map(g => g.name).join(', ')
})
/* 折叠面板控制,默认收起 */
const activePanels = ref<string[]>([])
/* ---------- 交互逻辑 ---------- */
/**
* 项目勾选变化时触发
* - 只处理项目本身的选中/取消
* - 方法的选中状态保持独立,不会自动勾选
*/
function onItemCheckChange(
data: any,
checked: boolean,
indeterminate: boolean
) {
const itemId = data.id as string
const itemLabel = data.label as string
if (checked) {
// 防止重复添加
if (!selectedGroups.value.find(g => g.id === itemId)) {
const methods: Method[] = (data.children ?? []).map((m: any) => ({
id: m.id,
name: m.label,
checked: false // 初始不选中
}))
selectedGroups.value.push({
id: itemId,
name: data.label, // 原始完整名称(含前缀,供 title 使用)
displayName: itemLabel, // 已去除前缀,供卡片展示
methods
})
}
} else {
expandedGroups.value.add(projectId)
// 取消选中,移除对应分组
selectedGroups.value = selectedGroups.value.filter(g => g.id !== itemId)
// 同时收起对应折叠面板
activePanels.value = activePanels.value.filter(id => id !== itemId)
}
}
// 监听项目选择变化,默认收起新选中的项目明细
watch(selectedProjectIds, (newIds, oldIds) => {
const added = newIds.filter(id => !oldIds.includes(id))
added.forEach(id => expandedGroups.value.delete(id))
}, { deep: true })
/**
* 方法勾选变化时触发
* - 仅更新对应方法的 checked 状态
* - 不会影响项目的选中状态
*/
function onMethodChange(group: ItemGroup, method: Method) {
// 方法状态已在 v-model 中同步,这里仅用于业务校验或后续处理
// 示例:若全部方法未选中,可提示用户
const anyChecked = group.methods.some(m => m.checked)
if (!anyChecked) {
// 可选:自动收起该分组的详情面板
activePanels.value = activePanels.value.filter(id => id !== group.id)
}
}
/**
* 删除已选项目卡片
*/
function removeGroup(group: ItemGroup) {
// 同步更新树的选中状态
const tree = (refs.itemTree as any).getCheckedKeys()
const newChecked = tree.filter((key: string) => key !== group.id)
;(refs.itemTree as any).setCheckedKeys(newChecked)
// 移除分组
selectedGroups.value = selectedGroups.value.filter(g => g.id !== group.id)
activePanels.value = activePanels.value.filter(id => id !== group.id)
}
/* ---------- 监听折叠面板打开/关闭 ---------- */
watch(
activePanels,
(newVal, oldVal) => {
// 当面板打开时,可在此处加载懒加载数据(如检查方法详情)
// 这里保持空实现,满足“默认收起”需求
}
)
</script>
<style scoped>
.exam-apply-container {
display: grid;
grid-template-columns: 200px 1fr 1fr;
grid-template-rows: auto 1fr;
gap: 16px;
.exam-apply {
padding: 16px;
height: 100%;
}
.category-panel, .project-panel, .method-panel {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
background: #fff;
}
.project-panel, .method-panel {
overflow-y: auto;
}
.selected-area {
grid-column: 1 / -1;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
background: #fff;
}
.project-list, .method-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.selected-card {
font-weight: 500;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.selected-details {
padding: 8px 12px 8px 24px;
background: #fafafa;
border-left: 2px solid #409eff;
margin-top: 12px;
}
.group-header {
font-weight: 600;
cursor: pointer;
}
.method-list {
list-style: none;
padding-left: 0;
}
.method-item {
padding: 4px 0;
color: #606266;
}
.toggle-icon {
font-size: 14px;
color: #909399;
}
.slide-fade-enter-active, .slide-fade-leave-active {
transition: all 0.2s ease;
}
.slide-fade-enter-from, .slide-fade-leave-to {
opacity: 0;
transform: translateY(-5px);
margin: 4px 0;
}
</style>