基于element-ui封装弹窗表格实现单选和多选

1,143 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

基于element-ui封装弹窗表格,实现多选和单选功能。

为了解决以下问题:

  • 日常场景需要用到翻页选择多条数据
  • 数据量巨大,考虑性能问题,后端无法一次性返回所有数据
  • 需要同时展示数据的编码、名称等内容
  • 搜索条件通过接口进行数据筛选

以上问题无法通过简单的下拉多选框或下拉单选框实现,因此使用弹窗表格实现单选和多选功能。

弹窗效果

选中后效果

一、组件封装

1、弹窗框架

涉及属性和方法:

  • title: 弹窗名称,
  • @open: Dialog 打开的回调
  • width: 弹窗宽度
  • dialogTop: Dialog CSS 中的 margin-top 值
  • model-value:是否显示 Dialog
  • before-close: Dialog 关闭前的回调

2、弹窗搜索条件封装

涉及属性和方法:

  • form: 搜索表单数据
  • formList: 搜索条件
  • getData: 获取列表数据
  • resetInfo: 重置搜索条件

3、操作数据、确认数据

涉及属性和方法:

  • isRadio: 判断是否单选
  • tableData: 列表数据
  • tableLoading: 获取列表数据加载loading
  • tableList: 表头字段
  • leftAdd:添加选中
  • rightAdd:减少选中
  • leftMIAnyJsonListItemAdd: 批量添加选中
  • rightMIAnyJsonListItemAdd: 批量减少选中
  • currentChange:单选情况下点击行为选中数据
  • handleClose: 多选情况下确认或取消

完整代码

  • 请求接口获取数据根据自身需求做处理、这里不多做添加
<template>
<el-dialog :title="title" @open="handleOpen" :width="dialogWidth" :top='dialogTop' :model-value="modelValue" :before-close="() => { handleClose('cancel') }">
  <section class="page-searchbox" v-if="formList.length > 0">
    <el-form ref="formRef" :inline="true" :model="form" @submit.prevent label-width="76px">
      <div class="form-seacher-flex-wrap">
          <el-form-item v-for="item in formList.filter((item) => item.isShow)" :key="item.key" :label="item.label" :prop="item.key">
            <el-input v-model="form[item.key]" @keyup.enter="getData" clearable maxlength="32" />
          </el-form-item>
          <el-form-item class="search-tools">
            <el-button type="primary" @click="getData">搜索</el-button>
            <el-button @click="resetInfo">重置</el-button>
          </el-form-item>
        </div>
    </el-form>
  </section>
  <div class="multiple-dialog" v-if="!isRadio">
    <div class="content-dialog">
      <div class="dialog-left" >
        <el-table row-key="code" border :data="tableData" v-loading="tableLoading" stripe highlightCurrentRow height="400">
          <el-table-column show-overflow-tooltip v-for="item in tableList.filter((item) => item.isShow)" :key="item.key" :prop="item.key" align="center" :label="item.label"></el-table-column>
          <el-table-column show-ovexrflow-tooltip align="center" width="60" label="操作" >
            <template v-slot="scope">
              <span class="action-btn" @click="leftAdd(scope.row)">+</span>
            </template>
          </el-table-column>
        </el-table>
        <el-pagination popper-class="pagination-popper" @size-change="handleSizeChange"  @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize"  :pager-count="5"  layout="total, prev, pager, next, sizes " :total="total" > </el-pagination>
      </div>
      <div class="dialog-middle">
        <el-button @click="leftManyAdd" :style="{ margin: '10px 0 10px 0' }" >《《 </el-button>
        <el-button @click="rightManyAdd"> 》》</el-button>
      </div>
      <div  class="dialog-right">
        <el-table row-key="code"  border :data="selectTableData" v-loading="tableLoading"  stripe highlightCurrentRow height="400" >
          <el-table-column show-overflow-tooltip v-for="item in tableList.filter((item) => item.isShow)" :key="item.key" :prop="item.key" align="center" :label="item.label"></el-table-column>
          <el-table-column show-ovexrflow-tooltip align="center"  width="60" label="操作" >
            <template v-slot="scope">
              <span class="action-btn" @click="rightAdd(scope.row)">-</span>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </div>
  <div class="radio-main" v-else-if="isRadio">
    <div class="dialog-table-content">
      <div>
        <el-table ref="multipleTable" row-key="code" border :data="tableData" v-loading="tableLoading" :row-class-name="({ row }) => row.disabled && 'disabled'" stripe height="400" @current-change="currentChange" >
          <el-table-column show-overflow-tooltip v-for="(item) in tableList.filter((item) => item.isShow)" :key="item.key" :prop="item.key" align="center" :label="item.label"></el-table-column>
        </el-table>
        <div class="table-page" >
          <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :pager-count="5" layout="total, prev, pager, next, sizes " :total="total" />
        </div>
      </div>
    </div>
  </div>
  <template #footer v-if="!isRadio">
    <span class="dialog-footer">
      <el-button type="primary" :loading="submitLoading" @click="handleClose('confirm')">确 定</el-button>
      <el-button @click="handleClose('cancel')">取 消</el-button>
    </span>
  </template>
</el-dialog>
</template>

<script lang='ts'>
  import { defineComponent, reactive, ref, toRefs, watch, defineAsyncComponent, nextTick } from "vue";
  import { funcApi } from "@/index/api";
  
  // 组件初始化数据接口
  interface ICompStateData {
    submitLoading: boolean;
    formList: Array<any>;
    form: any;
    tableData: Array<any>;
    // 选择后的
    selectTableData: Array<any>;
    currentPage: number;
    pageSize: number;
    total: number;
    formRef: any;
    tableLoading: boolean;
    tableList: Array<any>;
    dialogWidth: number;
    dialogTop:string;
  }
  
  export default defineComponent({
    name: "DialogCombination",
    emits: ['update:modelValue',"custHandle"],
    props: {
      modelValue: {
        type: Boolean,
        default: false,
      },
      title:{
        type:String,
        default:"",
      },
      dataContent:{
        type:Object as () => any,
        default:()=>({}),
      },
      showFormList:{
        type:Object as () => any,
          default:()=>({ code:true, name:true, }),
      },
      showTableList:{
        type:Object as () => any,
        default:()=>({ code:true, name:true, }),
      },
      isRadio: { // 是否单选 默认为单选
        type: Boolean,
        default:true
      },
      checkTable:{ // 回传选中的list,list中code是必须要有
        type:Array as () => Array<any>,
        default:()=>(null),
      },
  },
    setup(props, { emit }) {
      const state = reactive<ICompStateData>({
        submitLoading: false,
        formList: [
          { key: "code", label: "编码", isShow:props?.showFormList?.code? true :false },
          { key: "name", label: "名称", isShow:props?.showFormList?.name? true :false },
        ],
        tableList: [
          { key: "code", label: "编码", isShow:props?.showTableList?.code? true :false },
          { key: "name", label: "名称", isShow:props?.showTableList?.name? true :false },
        ],
        form: {
          name: "",
          code: "",
        },
        tableData: [],
        selectTableData: [],
        currentPage: 1,
        pageSize: 10,
        total: 0,
        formRef: ref(null),
        tableLoading: false,
        dialogWidth: 920,
        dialogTop: '8vh' ,
      });
      // 单选选中对应行
      const currentChange = (rowData: any) => {
        if(rowData && Object.keys(rowData).length > 0){// 这里只需要已存在的数据,清空的时候会调用这
          state.selectTableData = [rowData]
          emit("custHandle", { visible: false, list: state.selectTableData || [], rowData: state.selectTableData ? state.selectTableData[0] : {} });
          emit('update:modelValue', false);
        }
      };
      // 关闭弹窗
      const handleClose = (action: string): void => {
        if(action === "confirm"){
          emit("custHandle", { visible: false, list: state.selectTableData || [] });
          emit('update:modelValue', false);
        }else{
          emit('update:modelValue', false);
        }
      };
      // 操作添加
      const leftAdd = (data: any) => {
        const handleIndexSelect = state.selectTableData.findIndex( (item) => data.code === item.code );
        if (handleIndexSelect === -1) state.selectTableData.push(data);
      };
      // 操作减少
      const rightAdd = (data: any) => {
        const selectTableData = state.selectTableData;
        const handleIndexSelect = selectTableData.findIndex( (item) => data.code === item.code );
        handleIndexSelect !== -1 && state.selectTableData.splice(handleIndexSelect, 1);
      };
      // 操作添加
      const leftManyAdd = (): void => {
        const tableData = JSON.parse(JSON.stringify(state.tableData));
        const selectTableData = state.selectTableData;
        tableData.map((item: any) => {
          const handleIndexSelect = selectTableData.findIndex( (st) => st.code === item.code );
          handleIndexSelect !== -1 && selectTableData.splice(handleIndexSelect, 1); // 清空左边有存在相同的数据
        });
        state.selectTableData = selectTableData;
      };
      // 批量操作减少
      const rightManyAdd = (): void => {
        const selectTableData = state.selectTableData;
        let hashRow ={};
        const tableData = state.tableData.reduce((item:any,next:any)=>{
          hashRow[`${next.id}${next.code}`] ? '' : hashRow[`${next.id}${next.code}`]  = true && item.push(next)
          return item
        },[]);
        tableData.map((item: any) => {
          const handleIndexSelect = selectTableData.findIndex( (st) => st.code === item.code );
          handleIndexSelect !== -1 && selectTableData.splice(handleIndexSelect, 1);
        });
        state.selectTableData = selectTableData.concat(tableData);
      };
      
      //每页条
      const handleSizeChange = (sizeval: number) => {
        state.pageSize = sizeval;
        state.currentPage = 1;
        getData();
      };
      
      //当前页
      const handleCurrentChange = (currentVal: number) => {
        state.currentPage = currentVal;
        getData();
      };
      
      // 如果没有模板,取消弹窗并且提示错误
      const errorShow = () => {
        emit('update:modelValue', false);
        console.log('请联系管理员的分配对应模板!');
      }
      
      // 获取表格数据
    const getData = async (flag: any = false): Promise<void> => { // flag为了判断弹窗第一次获取数据
      try {
        let formData = state.form;
        state.tableLoading = true;
        const currentPage = state.currentPage-1;
        // 请求数据
        const beData = await funcApi({page_index: currentPage, page_size: state.pageSize, ...props.dataContent, ...formData}) ;
        if(beData.error) {errorShow();return;}
        const { rows, page_data: { count } } = beData;
        if ( count !== 0 && Math.ceil(count / state.pageSize) < state.currentPage ) {
          state.currentPage = 1;
          await getData();
        } else {
          state.tableData = rows;
          state.total = count;
        }
      } catch (error) {
        console.log("%c error", "color: chartreuse", error);
      }
      state.tableLoading = false;
    };
      // 重置搜索条件
      const resetInfo = () => {
        state.formRef.resetFields();
        state.currentPage = 1;
        getData();
      };
      
      // 参数 data 传入要该表的数据 list 表示原有的数据
      const getFirstList =(data:any, list :any [] )=>{
        let lt:any[] = [] ;
        list.map(item=>{
          if(data[`${item.key}`]){
            lt.push({ ...item, isShow:true })
          }else{
            lt.push({ ...item, isShow:false })
          }
        })
        return lt
      }
      // 打开弹窗重置条件并获取数据
      const handleOpen = ():void => {
        state.formRef?.resetFields();
        state.selectTableData = props.checkTable ? props.checkTable : [] ;
        state.tableData = [];
        state.currentPage = 1;
        state.total = 0;
        nextTick(()=>{ getData(true); })
      }
      
      watch(() => props.showFormList, () => {
        state.formList = JSON.parse(JSON.stringify(getFirstList(props.showFormList, state.formList))) ;
        // 初始form表单的信息
        state.formList.map(item=>{
          state.form[item.key] = '';
        });
      });
      
      watch(() => props.showTableList, () => {
        state.tableList = JSON.parse(JSON.stringify(getFirstList(props.showTableList, state.tableList)));
      });
      
      return {
        ...toRefs(state),
        handleClose,
        leftAdd,
        rightAdd,
        leftManyAdd,
        rightManyAdd,
        handleSizeChange,
        handleCurrentChange,
        getData,
        resetInfo,
        handleOpen,
        currentChange,
      };
    },
  });
</script>

<style lang="scss" scoped>
  @import "@/Common/Dialog/Combination/index.scss";
  .pagination-popper {
    z-index:999999 !important;
  }
</style>

@/Common/Dialog/Combination/index.scss

.multiple-dialog{
  .el-table .success-row {
    background: #f0f9eb;
  }
  .content-dialog {
    width: 100%;
    display: flex ;
    .dialog-left{
      width: 50%;
    };
    .dialog-middle {
      width: 10%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    };
    .dialog-right {
      width: 40%;
    };
    
    .action-button-text{
      width: 35px;
      height: 25px;
      font-size: 14px;
    }
  }
  :deep(.page-searchbox .el-input){
    width: 180px !important;
  }
  :deep(.el-form-item__content){
    width: 180px !important;
  }
  .search-tools{
    width: 144px;
  }
  
}

.radio-main{
  .el-table .success-row {
    background: #f0f9eb;
  }
  .dialog-table-content{
    width: 100% ;
    .table-page{
      display: flex;
      justify-content: flex-end
    }
  }
  :deep(.el-table__row:hover > td) {
    background-color: #B0C4DE !important;
  }
  
  :deep(.el-table__body tr.current-row>td) {
    background-color: #6CA6CD	 !important;
  }
  :deep(.page-searchbox .el-input){
    width: 180px !important;
  }
  
  :deep(.el-form-item__content){
    width: 180px !important;
  }
  
  .search-tools{
    width: 144px;
  }
}

二、组件使用

<el-input v-model="form[item.key]" maxlength="0" clearable @clear="()=>onSearchClear(item.clearData)">
  <template v-slot:append>
    <el-button icon="search" @click="onDialogShow(item.key)"></el-button>
  </template>
</el-input>
<Combination v-model="dialogVisible" @cust-handle="custDialog" v-bind="{ ...searchData }" clearable />
interface StateType{
  form: IAnyJsonListItem
  dialogVisible: boolean
  searchData: IAnyJsonListItem
  dialogKey: string
}
export default defineComponent({
  name: 'example',
  setup() {
    const state = reactive<StateType>({
      form: {},
      searchData: {},
      dialogVisible: false,
      dialogKey: '',
    });
    const onDialogShow = (key:string) => {
      state.dialogKey = key;
      switch(key){
        case 'customerCode': {
          state.searchData = { title:'选择客户', selectType: 'selectType', dataContent: { routineFilter: { level: 10 } } }
          state.dialogVisible = true;
          break;
        }
        default: break;
      }
    }
    const custDialog = (data: IAnyJsonListItem) => {
      const { list } = data;
      if(state?.form){
        let operaData = {}
        switch(state.dialogKey){
          case 'customerCode': {
            operaData = {
              customerCode: list.map((item: IAnyJsonListItem) => item.code),
              customerIdList: list.map((item: IAnyJsonListItem) => item.id),
            }
            break;
          }
          default: break;
        }
        state.form = JSON.parse(
          JSON.stringify({
            ...state.form,
            ...operaData
          })
        );
      }
    };
    const onSearchClear =(arg:IAnyJsonListItem, type: string)=>{
      if(arg){
        Object.keys(arg).map((item)=>{
          state.form[item] = arg[item];
        });
      }
    }
    return {
      ...toRefs(state),
      onDialogShow,
      custDialog,
    }
  }
})