左树右表弹窗组件

一、页面布局

二、树组件

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。下面是对代码的逐行解释:

  1. const filteredTreeData = computed(() => {:创建一个计算属性 filteredTreeData,它会根据内部逻辑返回值

  2. if (!searchValue.value) { return treeData.value; }:检查 searchValue 是否为空(搜索框是否输入值)。如果为空,返回完整的树数据 treeData.value

  3. const matchedNodes = [];:声明一个空数组 matchedNodes,用于存储经过过滤后的节点。

  4. const filterNodes = (nodes) => {:定义一个递归函数 filterNodes,它接受一个节点数组 nodes 作为参数,用于过滤这些节点。

  5. return nodes.reduce((acc, node) => {:使用 reduce 方法遍历 nodes 数组,将 acc 作为累加器。

  6. const matchedChildren = node.children ? filterNodes(node.children) : [];:检查当前节点是否有子节点。如果有,递归调用 filterNodes 来过滤子节点,结果存入 matchedChildren;如果没有子节点,则 matchedChildren 为一个空数组。

  7. if (node.title.includes(searchValue.value) || matchedChildren.length > 0) {:检查当前节点的标题是否包含搜索值,或者其子节点中是否有匹配项。

  8. acc.push({ ...node, children: matchedChildren });:如果当前节点匹配(标题中有搜索值)或有匹配的子节点,则将当前节点(包括其匹配的子节点)添加到累计器 acc 中。

  9. if (node.key && !expandedKeys.value.includes(node.key)) { expandedKeys.value.push(node.key); }:如果当前节点有键(key),且 expandedKeys 中没有该键,则将该键添加到 expandedKeys 中,以便在最终渲染时展开该节点。

  10. return acc;:返回累加器 acc,其中包含所有符合条件的节点。

  11. matchedNodes.push(...filterNodes(treeData.value));:调用 filterNodes,将根节点 treeData.value 作为输入,过滤并把结果(匹配的节点)添加到 matchedNodes 数组中。

  12. return matchedNodes;:返回经过筛选后的节点数组 matchedNodes,这就是最终的 filteredTreeData

总结:这段代码的主要功能是根据用户输入的搜索值过滤树形结构数据,返回符合条件的节点及其子节点,同时管理节点展开的状态。

3、根据选择不同树节点,传递不同的key值给testTable,vue组件

根据选择不同树节点,传递不同的key值给testTable,vue组件,使其表格实现根据key值显示对应的数据。

  1. 方法一:利用vuex状态管理器,调用 Test1StoresetSelectedKey 方法,将第一个选中的键值更新到 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. 方法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 (搜索框)的值。以下是对该代码的逐行解释:

  1. const shouldHideRow = (record) => {:定义一个名为 shouldHideRow 的箭头函数,该函数接受一个参数 record,代表当前要判断的记录。

  2. let result = ref(false);:使用 Vue 的 ref 创建一个响应式变量 result,初始值为 false。这个变量用于存储最终的隐藏判断结果。

  3. if (!typeKey.value || typeKey.value === '' || typeKey.value === '0') {:检查 typeKey 的值,如果 typeKey 是空值、空字符串或 '0',则进入该条件(树组件选择了全部)。

  4. result.value = !record;:如果上面的条件为真,则将 result.value 设置为 !record。这里的逻辑意味着如果 record 所有的行都不需要隐藏。

  5. 接下来的 else if 语句块根据不同的 typeKey 值来判断是否隐藏该行。每个条件比较 record 的属性(如 typematerial)来决定是否应该隐藏该行:

    • 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 的不同值来检查 recordmaterialtype 是否匹配特定值。

  6. if (searchValue.value) { result.value = !record.name.includes(searchValue.value); }:若 searchValue 有值,则检查 record.name 是否包含这个搜索值。如果不包含,则将 result.value 设置为真,即隐藏该行。

  7. return result.value;:返回最终的结果,表明该记录是否应该被隐藏(true 表示隐藏,false 表示显示)。

总结:整个逻辑的核心是根据 typeKeysearchValue 的值来判断在表格中某一行记录是否应被隐藏,通过检查记录的属性与条件进行匹配。该函数可以用于动态控制表格显示的内容。

ps:这里当时陷入了一个误区,将不需要显示的数据给过滤掉了而不是隐藏起来,导致后面选择数据时发生了cuo错误!!!浪费了许多时间解决。。。。

3、将选择的行数据存到vuex中

  1. state.selectedRowKeys = selectedRowKeys;:将传入的 selectedRowKeys 更新到组件的状态 state 中,state 这里可能是一个响应式对象,用于跟踪当前选中的行,这里主要是实现表格显示选中的数据前面打勾,并且记录选择了哪些数据。

  2. const selected = data.value.filter((item) => state.selectedRowKeys.includes(item.key));:从 data.value 中筛选出当前选中的行数据。使用 filter 方法,遍历 data.value(可能是一个包含所有行数据的数组),并仅保留那些键值在 selectedRowKeys 中的项,最终将选中的行数据存储在 selected 变量中。

  3. Test1Store.setSelectedRows(selected);:调用 Test1StoresetSelectedRows 方法,将选中的行数据更新到 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。

至此,左树右表弹窗组件完成。


左树右表弹窗组件
http://localhost:8090//archives/zuo-shu-you-biao-dan-chuang-zu-jian
作者
cchen
发布于
2024年10月04日
许可协议