<template>
<!-- 主弹窗 -->
<el-dialog
v-model="visible"
title="数据选择器"
width="75%"
top="5vh"
align-center
destroy-on-close
>
<div class="conn-container">
<!-- 左侧树形结构 -->
<div class="tree-panel">
<el-input
v-model="treeFilterText"
placeholder="输入机构名称"
clearable
prefix-icon="Search"
/>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
:filter-node-method="filterNode"
node-key="value"
highlight-current
@node-click="handleNodeClick"
/>
</div>
<!-- 右侧表格区域 -->
<div class="table-panel">
<!-- 数据表格 -->
<el-table
ref="mainTableRef"
:data="tableData"
style="width: 100%"
height="calc(60vh - 110px)"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="名称" min-width="180" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="date" label="日期" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
{{ row.status === "active" ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 表格操作栏 -->
<div class="table-toolbar">
<el-button
type="primary"
@click="showSelectedTable"
:disabled="selectedData.length === 0"
>
<el-icon><View /></el-icon>
已选择({{ selectedData.length }})
</el-button>
<!-- <div class="selection-controls">
<el-checkbox v-model="selectAllCurrentPage" @change="toggleSelectCurrentPage">
全选当前页
</el-checkbox>
<el-checkbox v-model="selectAllPages" @change="toggleSelectAllPages">
全选所有页({{ pagination.total }})
</el-checkbox>
</div> -->
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 20, 50]"
layout="sizes, prev, pager, next, jumper"
:total="pagination.total"
background
@size-change="fetchTableData"
@current-change="fetchTableData"
/>
</div>
<!-- 底部功能区 -->
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="confirmSelection">确认</el-button>
</div>
</div>
</div>
</el-dialog>
<!-- 已选数据弹窗 -->
<el-dialog
v-model="selectedDialogVisible"
title="已选择数据"
width="70%"
append-to-body
destroy-on-close
>
<div class="batch-operations">
<el-button type="danger" :disabled="selectedRows.length === 0" @click="batchRemoveSelected">
<el-icon><Delete /></el-icon>
批量删除
</el-button>
<span class="selected-count">已选择 {{ selectedData.length }} 项数据</span>
</div>
<el-table :data="selectedData" border @selection-change="handleSelectedTableSelection">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="名称" min-width="180" />
<el-table-column prop="type" label="类型" width="120" />
<el-table-column prop="date" label="日期" width="150" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ $index }">
<el-button type="danger" icon="Delete" circle @click="removeSelectedItem($index)" />
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="selectedDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, onMounted, nextTick } from "vue";
import { ElMessage, ElTree, ElTable, FilterNodeMethodFunction } from "element-plus";
import type { Node } from "element-plus/es/components/tree/src/tree.type";
import { View, Delete } from "@element-plus/icons-vue";
// 类型定义
interface TreeNode {
value: number | string;
label: string;
children?: TreeNode[];
}
interface TableData {
id: number | string;
name: string;
type: string;
date: string;
status: "active" | "inactive";
}
interface Pagination {
currentPage: number;
pageSize: number;
total: number;
}
// 弹窗显示控制
const visible = ref(false);
const selectedDialogVisible = ref(false);
// 树形结构数据
const treeFilterText = ref("");
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeData = ref<TreeNode[]>([
{
value: 1,
label: "一级节点",
children: [
{ value: 11, label: "二级节点1" },
{ value: 12, label: "二级节点2" },
{
value: 13,
label: "二级节点3",
children: [
{ value: 131, label: "三级节点1" },
{ value: 132, label: "三级节点2" },
],
},
],
},
{
value: 2,
label: "另一个一级节点",
children: [
{ value: 21, label: "二级节点A" },
{ value: 22, label: "二级节点B" },
],
},
]);
const treeProps = {
children: "children",
label: "value",
};
// 表格数据
const tableData = ref<TableData[]>([]);
const loading = ref(false);
const mainTableRef = ref();
// 分页配置
const pagination = reactive<Pagination>({
currentPage: 1,
pageSize: 10,
total: 0,
});
// 所有选中ID集合,全局存储选中项的ID(使用Set避免重复)
const allSelectedIds = ref<Set<string | number>>(new Set());
const selectedData = ref<TableData[]>([]); // 所有选中数据
const currentPageSelected = ref<TableData[]>([]); // 当前页选中数据
const selectedRows = ref<TableData[]>([]); // 在已选表格中选择的行
const selectAllCurrentPage = ref(false); // 全选当前页
const selectAllPages = ref(false); // 全选所有页
// 树节点搜索
// 树节点过滤
watch(treeFilterText, (val) => {
treeRef.value?.filter(val);
});
/**
* 筛选
*/
function filterNode(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
// 获取表格数据
const fetchTableData = async () => {
try {
loading.value = true;
// 模拟API请求
await new Promise((resolve) => setTimeout(resolve, 500));
// 生成模拟数据
const mockData: TableData[] = [];
const types = ["类型A", "类型B", "类型C", "类型D"];
const statuses: ("active" | "inactive")[] = ["active", "inactive"];
for (let i = 0; i < pagination.pageSize; i++) {
mockData.push({
id: i + (pagination.currentPage - 1) * pagination.pageSize,
name: `项目 ${i + 1 + (pagination.currentPage - 1) * pagination.pageSize}`,
type: types[Math.floor(Math.random() * types.length)],
date: `
2023-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`,
status: statuses[Math.floor(Math.random() * statuses.length)],
});
}
tableData.value = mockData;
pagination.total = 85; // 模拟总条数
// 更新选择状态
updateMainTableSelection();
} catch (error) {
ElMessage.error("数据加载失败");
} finally {
loading.value = false;
}
};
// 表格选择变化
const handleSelectionChange = (selection: TableData[]) => {
console.log("handleSelectionChange");
// 获取当前页所有ID
const currentIds = tableData.value.map((row) => row.id);
// 从全局集合中移除当前页所有ID
currentIds.forEach((id) => {
if (!allSelectedIds.value.has(id)) {
allSelectedIds.value.delete(id); // 从全局集合中移除
}
});
// 从全局集合中移除当前页数据
selectedData.value = selectedData.value.filter((item) => !currentIds.includes(item.id));
// 将当前页新选中的加入全局集合
currentPageSelected.value = selection;
// 更新全局选中ID集合
selection.forEach((item) => allSelectedIds.value.add(item.id));
// 更新选中数据
selectedData.value = Array.from(allSelectedIds.value)
.map((id) =>
[...currentPageSelected.value, ...selectedData.value].find((item) => item.id === id)
)
.filter(Boolean) as TableData[];
// 更新全选状态
selectAllCurrentPage.value = selection.length === tableData.value.length;
selectAllPages.value = allSelectedIds.value.size === pagination.total;
};
// 同步表格选中状态
const syncSelection = () => {
nextTick(() => {
if (!mainTableRef.value) return;
mainTableRef.value.clearSelection();
tableData.value.forEach((row) => {
if (allSelectedIds.value.has(row.id)) {
mainTableRef.value.toggleRowSelection(row, true);
}
});
});
};
// 已选表格中的选择变化
const handleSelectedTableSelection = (selection: TableData[]) => {
selectedRows.value = selection;
};
// 显示已选数据弹窗
const showSelectedTable = () => {
selectedDialogVisible.value = true;
};
// 删除单个已选项
const removeSelectedItem = (index: number) => {
const removedItem = selectedData.value.splice(index, 1)[0];
// 从全局ID集合中移除
allSelectedIds.value.delete(removedItem.id);
// 更新主表格选中状态
updateMainTableSelection();
// 如果删除的是当前页的数据,取消其在表格中的选中状态
if (tableData.value.some((item) => item.id === removedItem.id)) {
nextTick(() => {
const row = tableData.value.find((item) => item.id === removedItem.id);
if (row && mainTableRef.value) {
mainTableRef.value.toggleRowSelection(row, false);
}
});
}
ElMessage.success("已移除选择项");
};
// 批量删除已选项
const batchRemoveSelected = () => {
if (selectedRows.value.length === 0) return;
// 从全局ID集合中移除选中的行
const idsToRemove = new Set(selectedRows.value.map((item) => item.id));
idsToRemove.forEach((id) => allSelectedIds.value.delete(id));
// 更新选中数据
selectedData.value = selectedData.value.filter((item) => !idsToRemove.has(item.id));
ElMessage.success(`已移除 ${selectedRows.value.length} 个选项`);
selectedRows.value = [];
// 更新主表格选中状态
updateMainTableSelection();
};
// 更新主表格选中状态
const updateMainTableSelection = () => {
if (!mainTableRef.value) return;
// 先清除所有选中状态
mainTableRef.value.clearSelection();
// 重新选中当前页中已选择的数据
nextTick(() => {
// 重新设置当前页的选中状态
const selectedIds = allSelectedIds.value;
tableData.value.forEach((row) => {
if (selectedIds.has(row.id)) {
// 使用 setTimeout 0 确保每次操作都在下一个事件循环中执行
setTimeout(() => {
mainTableRef.value.toggleRowSelection(row, true);
}, 0);
}
});
});
// 更新全选状态
selectAllCurrentPage.value = currentPageSelected.value.length === tableData.value.length;
selectAllPages.value = allSelectedIds.value.size === pagination.total;
};
// 全选/取消全选当前页
const toggleSelectCurrentPage = () => {
if (selectAllCurrentPage.value) {
// 全选当前页
tableData.value.forEach((row) => {
allSelectedIds.value.add(row.id);
});
} else {
// 取消全选当前页
tableData.value.forEach((row) => {
allSelectedIds.value.delete(row.id);
});
}
// 更新选中数据
selectedData.value = Array.from(allSelectedIds.value)
.map(
(id) =>
tableData.value.find((item) => item.id === id) ||
selectedData.value.find((item) => item.id === id)
)
.filter(Boolean) as TableData[];
// 更新表格选中状态
updateMainTableSelection();
};
// 全选/取消全选所有页
const toggleSelectAllPages = () => {
if (selectAllPages.value) {
// 全选所有页
allSelectedIds.value = new Set(Array.from({ length: pagination.total }, (_, i) => i));
// 实际项目中需要从所有页获取数据
selectedData.value = [...tableData.value]; // 简化示例
} else {
// 取消全选所有页
allSelectedIds.value.clear();
selectedData.value = [];
}
// 更新表格选中状态
updateMainTableSelection();
};
// 确认选择
const confirmSelection = () => {
if (selectedData.value.length === 0) {
ElMessage.warning("请至少选择一条数据");
return;
}
emit("confirm", selectedData.value);
visible.value = false;
};
// 树节点点击事件
const handleNodeClick = (node: Node) => {
fetchTableData();
};
// 暴露打开方法
const open = () => {
visible.value = true;
// 重置选择状态
allSelectedIds.value.clear();
selectedData.value = [];
selectAllCurrentPage.value = false;
selectAllPages.value = false;
fetchTableData();
};
// 监听已选择数据变化,更新表格选中状态
watch(
selectedData,
() => {
selectAllPages.value = allSelectedIds.value.size === pagination.total;
},
{ deep: true }
);
// 初始化树形结构
onMounted(() => {
// 展开第一层节点
setTimeout(() => {
treeData.value.forEach((node) => {
treeRef.value?.store.nodesMap[node.value]?.expand();
});
}, 100);
});
// 定义事件
const emit = defineEmits<{
(e: "confirm", data: TableData[]): void;
}>();
defineExpose({ open });
</script>
<style scoped lang="scss">
.conn-container {
display: flex;
height: 100%;
gap: 16px;
}
:deep(.el-dialog) {
padding: 1px;
.el-dialog__header {
padding: 10px;
}
.el-dialog__body {
padding: 10px;
}
}
.tree-panel {
width: 30%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
}
.table-panel {
width: 70%;
display: flex;
flex-direction: column;
gap: 5px;
}
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 5px 0;
.el-button {
margin-right: auto;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--el-border-color-light);
gap: 12px;
}
.batch-operations {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.selected-count {
color: var(--el-text-color-secondary);
font-size: 14px;
}
}
/* 响应式调整 */
@media (max-width: 1200px) {
.dialog-body {
flex-direction: column;
height: auto;
}
.tree-panel,
.table-panel {
width: 100%;
padding: 0;
}
.tree-panel {
border-right: none;
border-bottom: 1px solid var(--el-border-color-light);
padding-bottom: 20px;
margin-bottom: 20px;
height: 40vh;
}
.table-toolbar {
flex-direction: column;
align-items: flex-start;
.selection-controls {
width: 100%;
justify-content: space-between;
margin: 10px 0;
}
}
}
/* 美化滚动条 */
.tree-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.tree-container::-webkit-scrollbar-thumb {
background-color: var(--el-color-primary-light-5);
border-radius: 3px;
}
.tree-container::-webkit-scrollbar-track {
background: var(--el-fill-color-lighter);
}
</style>
优化样式,树的宽度为当前页面的100
最新发布