左树右表弹窗组件
一、页面布局
二、树组件
1、代码展示
<template>
<a-card class="tree-container">
<a-input-search v-model:value="searchValue" style="margin-bottom: 8px" placeholder="搜索物料/产品类型" />
<a-tree
:show-line="showLine"
:show-icon="showIcon"
:tree-data="filteredTreeData"
:auto-expand-parent="autoExpandParent"
:default-expanded-keys="expandedKeys"
@select="onSelect"
>
<template #icon><carry-out-outlined /></template>
<template #title="{ dataRef }">
<template v-if="dataRef.key !== null">
<span v-if="dataRef.title.indexOf(searchValue) > -1">
{{ dataRef.title.substring(0, dataRef.title.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
{{ dataRef.title.substring(dataRef.title.indexOf(searchValue) + searchValue.length) }}
</span>
<span v-else>{{ dataRef.title }}</span>
</template>
</template>
<template #switcherIcon="{ dataRef, defaultIcon }">
<SmileTwoTone v-if="dataRef.key === '0-0-2'" />
<component :is="defaultIcon" v-else />
</template>
</a-tree>
</a-card>
</template>
<script setup>
import { ref, computed, defineEmits, watch } from 'vue';
const showLine = ref(true);
const showIcon = ref(false);
const searchValue = ref('');
const expandedKeys = ref(['0-0']);
const autoExpandParent = ref(true);
import { useTest1Store } from '/@/store/modules/system/test1';
const Test1Store = useTest1Store(); // 获取 Pinia store 实例
// 计算属性,用于绑定 store 中的 selectedRows
const storeSelectedKey = computed(() => Test1Store.key);
const key = ref('');
// 监视 store 的 selectedRows 的变化,更新本地 selectedRows
watch(storeSelectedKey, (newRows) => {
key.value = newRows;
console.log('watch storeSelectedRows', newRows);
});
const treeData = ref([
{
title: '全部',
key: '0',
children: [
{
title: '物料',
key: '0-0',
children: [
{
title: '金属',
key: '0-0-0',
},
{
title: '塑料',
key: '0-0-1',
},
{
title: '木材',
key: '0-0-2',
},
],
},
{
title: '产品',
key: '0-1',
children: [
{
title: '易碎品',
key: '0-1-0',
},
{
title: '办公用品',
key: '0-1-1',
},
],
},
],
},
]);
// TODO: 这段代码还是不太懂,后续研究一下
const filteredTreeData = computed(() => {
if (!searchValue.value) {
return treeData.value; // 如果输入框为空,返回完整树结构
}
const matchedNodes = [];
const filterNodes = (nodes) => {
return nodes.reduce((acc, node) => {
const matchedChildren = node.children ? filterNodes(node.children) : [];
// 如果当前节点匹配,或有匹配的子节点
if (node.title.includes(searchValue.value) || matchedChildren.length > 0) {
// 添加父节点到 matchedNodes
acc.push({ ...node, children: matchedChildren });
// 添加父节点的键到 expandedKeys
if (node.key && !expandedKeys.value.includes(node.key)) {
expandedKeys.value.push(node.key);
}
}
return acc;
}, []);
};
matchedNodes.push(...filterNodes(treeData.value));
return matchedNodes;
});
// 定义 emit 事件
// const emit = defineEmits(['selectKey']);
const onSelect = (selectedKeys, info) => {
console.log('selected', selectedKeys, info);
// emit('selectKey', selectedKeys[0]); // 发射 'selectKey' 事件并传递选中的第一个键值
console.log('emit', selectedKeys[0]);
// 更新 Pinia store 的 selectedRows
Test1Store.setSelectedKey(selectedKeys[0]);
};
</script>
<style scoped lang="less">
.tree-container {
height: 500px;
.tree {
height: 618px;
margin-top: 10px;
overflow-x: hidden;
}
.sort-flag-row {
margin-top: 10px;
margin-bottom: 10px;
}
.sort-span {
margin-left: 5px;
}
.no-data {
margin: 10px;
}
}
</style>
2、搜索功能实现
使用 Vue.js 的 computed
函数创建一个计算属性 filteredTreeData
,目的是根据用户在输入框中输入的搜索值来过滤树形结构数据 treeData
。下面是对代码的逐行解释:
const filteredTreeData = computed(() => {
:创建一个计算属性filteredTreeData,它会根据内部逻辑返回值
。if (!searchValue.value) { return treeData.value; }
:检查searchValue
是否为空(搜索框是否输入值)。如果为空,返回完整的树数据treeData.value
。const matchedNodes = [];
:声明一个空数组matchedNodes
,用于存储经过过滤后的节点。const filterNodes = (nodes) => {
:定义一个递归函数filterNodes
,它接受一个节点数组nodes
作为参数,用于过滤这些节点。return nodes.reduce((acc, node) => {
:使用reduce
方法遍历nodes
数组,将acc
作为累加器。const matchedChildren = node.children ? filterNodes(node.children) : [];
:检查当前节点是否有子节点。如果有,递归调用filterNodes
来过滤子节点,结果存入matchedChildren
;如果没有子节点,则matchedChildren
为一个空数组。if (node.title.includes(searchValue.value) || matchedChildren.length > 0) {
:检查当前节点的标题是否包含搜索值,或者其子节点中是否有匹配项。acc.push({ ...node, children: matchedChildren });
:如果当前节点匹配(标题中有搜索值)或有匹配的子节点,则将当前节点(包括其匹配的子节点)添加到累计器acc
中。if (node.key && !expandedKeys.value.includes(node.key)) { expandedKeys.value.push(node.key); }
:如果当前节点有键(key),且expandedKeys
中没有该键,则将该键添加到expandedKeys
中,以便在最终渲染时展开该节点。return acc;
:返回累加器acc
,其中包含所有符合条件的节点。matchedNodes.push(...filterNodes(treeData.value));
:调用filterNodes
,将根节点treeData.value
作为输入,过滤并把结果(匹配的节点)添加到matchedNodes
数组中。return matchedNodes;
:返回经过筛选后的节点数组matchedNodes
,这就是最终的filteredTreeData
。
总结:这段代码的主要功能是根据用户输入的搜索值过滤树形结构数据,返回符合条件的节点及其子节点,同时管理节点展开的状态。
3、根据选择不同树节点,传递不同的key值给testTable,vue组件
根据选择不同树节点,传递不同的key值给testTable,vue组件,使其表格实现根据key值显示对应的数据。
方法一:利用vuex状态管理器,调用
Test1Store
的setSelectedKey
方法,将第一个选中的键值更新到 Pinia 状态管理的 store 中。后续在testTable中可以调用key值。(最终觉得这个方法比较规范,使用了该方法)const Test1Store = useTest1Store(); // 获取 Pinia store 实例 // 计算属性,用于绑定 store 中的 selectedRows const storeSelectedKey = computed(() => Test1Store.key); const key = ref(''); // 监视 store 的 selectedRows 的变化,更新本地 selectedRows watch(storeSelectedKey, (newRows) => { key.value = newRows; console.log('watch storeSelectedRows', newRows); }); const onSelect = (selectedKeys, info) => { console.log('emit', selectedKeys[0]); // 更新 Pinia store 的 selectedRows Test1Store.setSelectedKey(selectedKeys[0]); };
方法2:先将key值通过子传父传递给父组件,再由父组件传递给子组件testTable.vue
// 定义 emit 事件 const emit = defineEmits(['selectKey']); const onSelect = (selectedKeys, info) => { console.log('selected', selectedKeys, info); emit('selectKey', selectedKeys[0]); // 发射 'selectKey' 事件并传递选中的第一个键值 console.log('emit', selectedKeys[0]); };
三、表格组件
1、代码展示
<template>
<a-card class="table-container">
<a-input-search v-model:value="searchValue" placeholder="搜索物料/产品" style="width: 280px" @search="onSearch" />
<!-- <a-button type="primary" :disabled="!hasSelected" :loading="state.loading" style="margin-left: 10px" @click="start"> 取消选择 </a-button> -->
<div class="table-wrapper">
<a-table
:row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: onSelectChange }"
:columns="columns"
:data-source="data"
:scroll="{ y: 280, x: 1000 }"
:row-class-name="(record) => (shouldHideRow(record) ? 'hidden-row' : '')"
/>
</div>
</a-card>
</template>
<script setup>
import { ref, computed, reactive, defineProps, watch } from 'vue';
import { useTest1Store } from '/@/store/modules/system/test1';
const Test1Store = useTest1Store(); // 获取 Pinia store 实例
const selectedRows = ref([]); // 本地的 selectedRows
// const isShow = ref(true); // 控制隐藏行的显示/隐藏
// 接收父组件传递的props(不同类型的物料/产品参数)
// const props = defineProps({
// selectedKey: {
// type: String,
// required: false, // 根据需要可以设置为必需或不必需
// },
// });
// watch(
// () => props.selectedKey,
// (newKey) => {
// console.log('Updated selectedKey:', newKey);
// }
// );
// 搜索框
const searchValue = ref('');
const shouldHideRow = (record) => {
// 在这里根据 record 的属性添加自定义的隐藏条件
// 比如:如果库存数量为 0,则隐藏该行
// return !typeKey.value || typeKey.value === '' || typeKey.value === '0'; // 示例条件
let result = ref(false);
if (!typeKey.value || typeKey.value === '' || typeKey.value === '0') {
console.log('隐藏行', record);
result.value = !record
} else if (typeKey.value === '0-0') {
result.value = record.type !== '物料'
} else if (typeKey.value === '0-0-0') {
result.value = record.material !== '金属件'
} else if (typeKey.value === '0-0-1') {
result.value = record.material !== '塑料'
} else if (typeKey.value === '0-0-2') {
result.value = record.material !== '木材'
} else if (typeKey.value === '0-1') {
result.value = record.type !== '产品'
} else if (typeKey.value === '0-1-0') {
result.value = record.material !== '易碎品'
} else if (typeKey.value === '0-1-1') {
result.value = record.material !== '办公用品'
}
if (searchValue.value) {
result.value = !record.name.includes(searchValue.value)
}
return result.value;
};
// 表格
const columns = [
{
title: '名称',
dataIndex: 'name',
fixed: 'left',
},
{
title: '库存数量',
dataIndex: 'stock',
},
{
title: '库存位置',
dataIndex: 'address',
},
];
// 所有物料和产品
const data = ref([
{
key: 0,
type: '物料',
material: '金属件',
name: '螺丝',
stock: 1132,
address: '库区1',
},
{
key: 1,
name: '螺帽',
type: '物料',
material: '金属件',
stock: 332,
address: '库区1',
},
{
key: 2,
name: '刀片',
type: '物料',
material: '金属件',
stock: 3452,
address: '库区2',
},
{
key: 3,
name: '包装袋',
type: '物料',
material: '塑料',
stock: 562,
address: '库区3',
},
{
key: 4,
name: '木块',
type: '物料',
material: '木材',
stock: 32,
address: '库区4',
},
{
key: 5,
name: '水杯',
type: '产品',
material: '易碎品',
stock: 92,
address: '库区5',
},
{
key: 6,
name: '笔',
type: '产品',
material: '办公用品',
stock: 23,
address: '库区6',
},
]);
// 取消选中按钮(用不到先注释掉,后面需要再进行功能实现)
// const hasSelected = computed(() => state.selectedRowKeys.length > 0);
// const start = () => {
// state.selectedRowKeys = [];
// };
// 计算属性,用于绑定 store 中的 selectedRows
const storeSelectedRows = computed(() => Test1Store.selectedRows);
// 计算属性,用于绑定 store 中的 selectedRows
const storeSelectedKey = computed(() => Test1Store.key);
const typeKey = ref('');
watch([storeSelectedKey, storeSelectedRows], ([newKey, newRows]) => {
// 更新 key 的值
typeKey.value = newKey;
console.log('Updated selectedKey:', newKey);
// 更新本地的 selectedRows
selectedRows.value = newRows;
console.log('watch storeSelectedRows', newRows);
// 更新 selectedRowKeys
state.selectedRowKeys = newRows.map((item) => item.key);
console.log('state.selectedRowKeys', state.selectedRowKeys);
});
const state = reactive({
selectedRowKeys: [], // 用于存储选中的行的 key
loading: false,
});
// 创建数组来存放不同属性的物料/产品的key值
const onSelectChange = (selectedRowKeys) => {
console.log('selectedRowKeys changed: ', selectedRowKeys);
state.selectedRowKeys = selectedRowKeys;
// 获取选中的行数据
const selected = data.value.filter((item) => state.selectedRowKeys.includes(item.key));
// 打印选中的行数据
console.log('Selected Rows:', selected);
// 更新 Pinia store 的 selectedRows
Test1Store.setSelectedRows(selected);
const arr = Test1Store.selectedRows.map((item) => item.key);
console.log(arr);
};
</script>
<style>
.table-container {
height: 500px;
}
.table-wrapper {
height: 100px;
}
.hidden-row {
display: none; /* 隐藏行 */
}
</style>
2、搜索功能与根据key值渲染表格数据数据实现
定义了一个名为 shouldHideRow
的函数,用于判断特定记录(某一行数据)(record
)是否应该被隐藏。其逻辑主要基于 typeKey
(vuex中树组件传递的key值)和 searchValue
(搜索框)的值。以下是对该代码的逐行解释:
const shouldHideRow = (record) => {
:定义一个名为shouldHideRow
的箭头函数,该函数接受一个参数record
,代表当前要判断的记录。let result = ref(false);
:使用 Vue 的ref
创建一个响应式变量result
,初始值为false
。这个变量用于存储最终的隐藏判断结果。if (!typeKey.value || typeKey.value === '' || typeKey.value === '0') {
:检查typeKey
的值,如果typeKey
是空值、空字符串或'0'
,则进入该条件(树组件选择了全部)。result.value = !record;
:如果上面的条件为真,则将result.value
设置为!record
。这里的逻辑意味着如果record
所有的行都不需要隐藏。接下来的
else if
语句块根据不同的typeKey
值来判断是否隐藏该行。每个条件比较record
的属性(如type
和material
)来决定是否应该隐藏该行:else if (typeKey.value === '0-0') { result.value = record.type !== '物料'; }
:若typeKey
是'0-0'
,则只有record.type
为'物料'
时不隐藏。else if (typeKey.value === '0-0-0') { result.value = record.material !== '金属件'; }
:typeKey
为'0-0-0'
时,只有record.material
为'金属件'
时不隐藏。其他条件类似,都根据
typeKey
的不同值来检查record
的material
或type
是否匹配特定值。
if (searchValue.value) { result.value = !record.name.includes(searchValue.value); }
:若searchValue
有值,则检查record.name
是否包含这个搜索值。如果不包含,则将result.value
设置为真,即隐藏该行。return result.value;
:返回最终的结果,表明该记录是否应该被隐藏(true
表示隐藏,false
表示显示)。
总结:整个逻辑的核心是根据 typeKey
和 searchValue
的值来判断在表格中某一行记录是否应被隐藏,通过检查记录的属性与条件进行匹配。该函数可以用于动态控制表格显示的内容。
ps:这里当时陷入了一个误区,将不需要显示的数据给过滤掉了而不是隐藏起来,导致后面选择数据时发生了cuo错误!!!浪费了许多时间解决。。。。
3、将选择的行数据存到vuex中
state.selectedRowKeys = selectedRowKeys;
:将传入的selectedRowKeys
更新到组件的状态state
中,state
这里可能是一个响应式对象,用于跟踪当前选中的行,这里主要是实现表格显示选中的数据前面打勾,并且记录选择了哪些数据。const selected = data.value.filter((item) => state.selectedRowKeys.includes(item.key));
:从data.value
中筛选出当前选中的行数据。使用filter
方法,遍历data.value
(可能是一个包含所有行数据的数组),并仅保留那些键值在selectedRowKeys
中的项,最终将选中的行数据存储在selected
变量中。Test1Store.setSelectedRows(selected);
:调用Test1Store
的setSelectedRows
方法,将选中的行数据更新到 Pinia 状态管理的 store 中。此操作使得其他组件(f父组件)或逻辑能够访问当前的选中行数据。
三、弹窗页面(父组件)
1、代码展示
<template>
<div>
<a-button type="primary" @click="showModal">弹窗1</a-button>
<a-modal v-model:open="open" title="选择物料/产品" @cancel="handleCancel" @ok="handleOk" class="test-modal" :width="1200">
<a-row :gutter="16">
<a-col :span="5">
<testTree @selectKey="handleSelectKey" />
</a-col>
<a-col :span="19">
<testTable :selectedKey="selectedKey" />
</a-col>
</a-row>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, watch, defineComponent } from 'vue';
import testTree from './components/testTree.vue';
import testTable from './components/testTable.vue';
// import { test1Store } from '/@/store/modules/business/test1'; // 假设您在此路径下定义了 store
import { useTest1Store } from '/@/store/modules/system/test1';
const open = ref(false);
// const selectedKey = ref(null); // 用于接收选中的键值
const store = useTest1Store(); // 获取 store 实例
const selectedRows = ref([]); // 本地的 selectedRows
// 计算属性以获取 count 值
const count = computed(() => store.count); // 从 store 中获取 count
console.log('111', count.value);
// 监听 selectedKey 的变化,更新 store 中的 selectedKey
// watch(selectedKey, (newKey) => {
// store.setSelectedRows(newKey);
// });
const showModal = () => {
open.value = true;
};
const handleOk = (e) => {
console.log(e);
// 点击确定按钮时,获取 store 中的 selectedRows
selectedRows.value = store.selectedRows; // 将 store 的 selectedRows 赋值给本地的 selectedRows
store.setSelectedRows([]); // 清空 store 中的 selectedRows
console.log('确认按钮点击,当前选中的行数据:', selectedRows.value); // 打印选中的行数据
open.value = false; // 关闭模态框
};
// 点击取消按钮或者关闭模态框时,清空 selectedRows
const handleCancel = () => {
// selectedRows.value = []; // 清空本地的 selectedRows
store.setSelectedRows([]); // 清空 store 中的 selectedRows
console.log('取消或关闭模态框,清空选中行数据');
};
</script>
<style>
.test-modal .ant-modal-body {
height: 500px;
/* overflow-y: scroll; */
padding: 0;
}
</style>
该页面主要实现用户点击确定按钮时,将在表格组件中选择的行数据保持到本地,并且清空vuex中保存的行数据,当用户点击取消按钮或者关闭模态框时,清空 store 中的 selectedRows。
至此,左树右表弹窗组件完成。