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