html
<template>
<el-dialog
v-model="visible"
title="搜索项配置"
width="900px"
class="custom-transfer-dialog"
@open="initData"
align-center
>
<div class="transfer-container">
<div class="transfer-panel left-panel">
<div class="panel-header">
<div class="header-left-group">
<el-checkbox
v-model="isAllLeftChecked"
:indeterminate="isLeftIndeterminate"
@change="handleLeftAllChange"
>
全部待选项
</el-checkbox>
</div>
<span class="count">{{ checkedLeft.length }}/{{ leftListFiltered.length }}</span>
</div>
<div class="panel-body">
<div class="search-bar-body">
<el-input
v-model="leftSearchKeyword"
placeholder="请输入关键词"
:prefix-icon="Search"
clearable
/>
</div>
<div class="list-container">
<el-checkbox-group v-model="checkedLeft">
<div v-for="item in leftListFiltered" :key="item.key" class="list-item">
<el-checkbox :label="item.key">
{{ item.label }}
</el-checkbox>
</div>
<div v-if="leftListFiltered.length === 0" class="empty-text">无匹配数据</div>
</el-checkbox-group>
</div>
</div>
</div>
<div class="transfer-buttons">
<el-button
type="primary"
:disabled="checkedLeft.length === 0"
class="operate-btn"
@click="moveToRight"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
<el-button
type="info"
plain
:disabled="checkedRight.length === 0"
class="operate-btn"
@click="moveToLeft"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
</div>
<div class="transfer-panel right-panel">
<div class="panel-header right-header-layout">
<div class="header-check-area">
<el-checkbox
v-model="isAllRightChecked"
:indeterminate="isRightIndeterminate"
@change="handleRightAllChange"
>
已选搜索项
</el-checkbox>
<span class="count">已选 {{ checkedRight.length }}/{{ rightListFiltered.length }}</span>
</div>
<div class="header-search-area">
<el-input
v-model="rightSearchKeyword"
placeholder="请输入字段名称"
:prefix-icon="Search"
size="small"
clearable
class="header-input"
/>
</div>
</div>
<div class="panel-body">
<div class="list-container full-height">
<el-checkbox-group v-model="checkedRight">
<div v-for="item in rightListFiltered" :key="item.key" class="list-item">
<el-checkbox :label="item.key">
{{ item.label }}
</el-checkbox>
</div>
<div v-if="rightListFiltered.length === 0" class="empty-text">无数据</div>
</el-checkbox-group>
</div>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useSearchStore } from '../../stores/searchStore'
import { Search, ArrowRight, ArrowLeft } from '@element-plus/icons-vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const searchStore = useSearchStore()
// === 状态定义 ===
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const currentTargetKeys = ref([]) // 右侧实际包含的 Key
const checkedLeft = ref([]) // 左侧当前勾选的 Key
const checkedRight = ref([]) // 右侧当前勾选的 Key
const leftSearchKeyword = ref('')
const rightSearchKeyword = ref('')
// 全选状态
const isAllLeftChecked = ref(false)
const isLeftIndeterminate = ref(false)
const isAllRightChecked = ref(false)
const isRightIndeterminate = ref(false)
// === 数据源计算 ===
// 1. 左侧列表:所有字段 减去 已在右侧的字段
const leftList = computed(() => {
return searchStore.allFields.filter((f) => !currentTargetKeys.value.includes(f.key))
})
const leftListFiltered = computed(() => {
if (!leftSearchKeyword.value) return leftList.value
return leftList.value.filter((item) =>
item.label.toLowerCase().includes(leftSearchKeyword.value.toLowerCase()),
)
})
// 2. 右侧列表:根据 currentTargetKeys 获取完整对象
const rightList = computed(() => {
return currentTargetKeys.value
.map((key) => searchStore.allFields.find((f) => f.key === key))
.filter(Boolean)
})
const rightListFiltered = computed(() => {
if (!rightSearchKeyword.value) return rightList.value
return rightList.value.filter((item) =>
item.label.toLowerCase().includes(rightSearchKeyword.value.toLowerCase()),
)
})
// === 监听与逻辑 ===
// 初始化
const initData = () => {
currentTargetKeys.value = [...searchStore.activeFieldKeys]
checkedLeft.value = []
checkedRight.value = []
leftSearchKeyword.value = ''
rightSearchKeyword.value = ''
updateLeftCheckState()
updateRightCheckState()
}
// 左侧 Checkbox 状态逻辑
watch(checkedLeft, () => updateLeftCheckState())
const updateLeftCheckState = () => {
const checkedCount = checkedLeft.value.length
const totalCount = leftListFiltered.value.length
isAllLeftChecked.value = totalCount > 0 && checkedCount === totalCount
isLeftIndeterminate.value = checkedCount > 0 && checkedCount < totalCount
}
const handleLeftAllChange = (val) => {
checkedLeft.value = val ? leftListFiltered.value.map((i) => i.key) : []
isLeftIndeterminate.value = false
}
// 右侧 Checkbox 状态逻辑 (新增)
watch(checkedRight, () => updateRightCheckState())
const updateRightCheckState = () => {
const checkedCount = checkedRight.value.length
const totalCount = rightListFiltered.value.length
isAllRightChecked.value = totalCount > 0 && checkedCount === totalCount
isRightIndeterminate.value = checkedCount > 0 && checkedCount < totalCount
}
const handleRightAllChange = (val) => {
checkedRight.value = val ? rightListFiltered.value.map((i) => i.key) : []
isRightIndeterminate.value = false
}
// 移动逻辑
const moveToRight = () => {
currentTargetKeys.value.push(...checkedLeft.value)
checkedLeft.value = []
// 清空左侧搜索,体验更好,也可不保留
// leftSearchKeyword.value = ''
}
const moveToLeft = () => {
currentTargetKeys.value = currentTargetKeys.value.filter(
(key) => !checkedRight.value.includes(key),
)
checkedRight.value = []
}
const handleConfirm = () => {
searchStore.updateActiveFields(currentTargetKeys.value)
visible.value = false
}
</script>
<style scoped>
.transfer-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 500px;
}
/* 左右面板基础 */
.transfer-panel {
width: 45%;
height: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
background: #fff;
overflow: hidden;
}
/* 标题栏通用 */
.panel-header {
height: 48px; /* 稍微加高一点以容纳搜索框 */
background: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
display: flex;
align-items: center;
padding: 0 12px;
font-size: 14px;
color: #606266;
}
/* 左侧标题布局 */
.panel-header {
justify-content: space-between;
}
/* 右侧标题布局:Flex 布局让搜索框靠右 */
.right-header-layout {
justify-content: space-between;
padding-right: 8px; /* 右侧稍微留点空隙 */
}
.header-check-area {
display: flex;
align-items: center;
gap: 12px; /* Checkbox和计数之间的间距 */
}
/* 搜索框区域 */
.header-search-area {
width: 140px; /* 限制头部搜索框宽度 */
}
.count {
font-size: 12px;
color: #909399;
}
/* 面板内容区 */
.panel-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 8px;
overflow: hidden;
}
/* 左侧内部搜索框 */
.search-bar-body {
margin-bottom: 8px;
}
.list-container {
flex: 1;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 2px;
}
.list-container.full-height {
/* 右侧没有内部搜索框,列表直接占满 */
margin-top: 0;
}
.list-item {
padding: 6px 10px;
cursor: pointer;
}
.list-item:hover {
background-color: #f5f7fa;
}
/* 按钮区 */
.transfer-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.operate-btn {
margin-left: 0 !important;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
}
.empty-text {
text-align: center;
color: #909399;
margin-top: 20px;
font-size: 13px;
}
/* 深度样式 */
:deep(.el-checkbox__label) {
padding-left: 8px;
color: #333;
font-weight: normal;
}
/* 调整头部输入框样式 */
:deep(.header-input .el-input__wrapper) {
background-color: #fff;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
</style>