Element Plus列表封装

120 阅读1分钟

hooks

useConfirm.ts

import { ElMessageBox, ElMessage } from "element-plus";
import { ResultEnum } from '/@/enums/httpEnum';

/**
 * @description 操作单条数据信息 (二次确认【删除、禁用、启用、重置密码】)
 * @param {Function} api 操作数据接口的api方法 (必传)
 * @param {Object} params 携带的操作数据参数 {id,params} (必传)
 * @param {String} message 提示信息 (必传)
 * @param {String} confirmType icon类型 (不必传,默认为 warning)
 * @returns {Promise}
 */
const useConfirm = (
  api: (params: any) => Promise<any>,
  params: any = {},
  message: string,
  confirmType: 'warning'
) => {
  return new Promise((resolve, reject) => {
    ElMessageBox.confirm(`${message}`, "温馨提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: confirmType,
      draggable: true
    }).then(async () => {
      const res = await api(params);
      if (!res) return reject(false);
      ElMessage({
        type: "success",
        message: `操作成功!`
      });
      resolve(true);
    });
  });
};

export default useConfirm;

useSelection.ts

import { ref, computed } from "vue";

/**
 * @description 表格多选数据操作
 * @param {String} rowKey 当表格可以多选时,所指定的 guid
 * */
const useSelection = (rowKey: string = 'guid') => {
  const isSelected = ref<boolean>(false);
  const selectedList = ref<{ [key: string]: any }[]>([]);

  // 当前选中的所有 ids 数组
  const selectedListIds = computed((): string[] => {
    return selectedList.value.map(item => item[rowKey]);
  });

  // 列表选中行数
  const selectListCount = computed((): number => {
    return selectedList.value.length;
  });

  /**
   * @description 多选操作
   * @param {Array} rowArr 当前选择的所有数据
   * @return void
   */
  const onSelectionChange = (rowArr: { [key: string]: any }[]) => {
    rowArr.length ? (isSelected.value = true) : (isSelected.value = false);
    selectedList.value = rowArr;
  };

  return {
    isSelected,
    selectedList,
    selectedListIds,
    selectListCount,
    onSelectionChange
  };
};

export default useSelection;

useTable.ts

import { computed, reactive, toRefs } from "vue";

// 默认分页
const DEFAULT_PAGE_SIZE = 10;

export namespace Table {
  export interface Pagination {
    pageIndex: number;
    pageSize: number;
    total: number;
  }
  export interface StateProps {
    loading: Boolean;
    searchParam: Object;
    totalParam: Object;
    tableData: any[];
    pages: Pagination;
  }
}

/**
 * @description table 页面操作方法封装
 * @param {Function} api 获取表格数据 api 方法(必传)
 * @param {Object} initParam 获取数据查询参数(非必传,默认为{})
 * @param {Boolean} isShowPage 是否有分页(非必传,默认为true)
 * @param {Function} onError 对后台返回报错处理的方法(非必传)
 * */
export const useTable = (
  api: (params: any) => Promise<any>,
  initParam: object = {},
  immediate: boolean = true,
  isShowPage: boolean = true,
  onError?: (error: any) => void
) => {
  const state = reactive<Table.StateProps>({
    loading: false,
    searchParam: {}, // 查询参数(只包括查询)
    totalParam: {}, // 查询参数(包括分页查询)
    tableData: [],
    pages: {
      pageIndex: 1,
      pageSize: DEFAULT_PAGE_SIZE,
      total: 0
    }
  });

  /**
   * @description 分页查询参数(只包括分页和表格字段排序,其他排序方式可自行配置)
   * */
  const pageParam = computed(() => {
    return {
      pageIndex: state.pages.pageIndex,
      pageSize: state.pages.pageSize
    };
  });

  /**
   * @description 获取表格数据
   * @return void
   * */
  const getTableList = async () => {
    if (!api) return;
    state.loading = true;
    try {
      // 先把初始化参数和分页参数放到总参数里面
      Object.assign(state.totalParam, initParam, isShowPage ? pageParam.value : {});
      const { pages, data } = await api({ ...state.totalParam });
      state.tableData = data ?? [];
      // 解构后台返回的分页数据 (如果有分页更新分页信息)
      isShowPage && updatePages(pages);
    } catch (error) {
      onError && onError(error);
    } finally {
      state.loading = false;
    }
  };

  // 是否立即触发
  const isImmediate = immediate ?? true;
  onMounted(() => isImmediate && getTableList());

  /**
   * @description 更新分页信息
   * @param {Object} pages 后台返回的分页数据
   * @return void
   * */
  const updatePages = (pages: Table.Pagination) => {
    Object.assign(state.pages, pages);
  };

  /**
   * @description 表格数据查询
   * @return void
   * */
  const onSearch = () => {
    state.pages.pageIndex = 1;
    getTableList();
  };

  /**
   * @description 表格数据重置
   * @return void
   * */
  const onReset = () => {
    state.pages.pageIndex = 1;
    getTableList();
  };

  /**
   * @description 表格数据刷新
   * @return void
   * */
  const onRefresh = () => {
    getTableList();
  };

  /**
   * @description 分页组件改变
   * @param {Object} pageIndex 当前页数
   * @param {Object} pageSize 当前条数
   * @return void
   * */
  const onPageChange = (pages: TableDemoPageType) => {
    state.pages.pageIndex = pages.pageIndex;
    state.pages.pageSize = pages.pageSize;
    getTableList();
  };

  return {
    ...toRefs(state),
    getTableList,
    onPageChange,
    onSearch,
    onReset,
    onRefresh
  }
};

export default useTable;

customTable封装

<template>
  <div :class="[!setBorder && 'el-table-no-border', 'table-container']">
    <header class="table-header mb12">
      <div class="table-header-operation">
        <slot name="operation-list" />
      </div>
      <div  v-if="showTools" class="table-header-tool">
        <SvgIcon
          name="iconfont icon-shuaxin"
          :size="22"
          title="刷新"
          @click="onRefreshTable"
        />
        <SvgIcon
          name="iconfont icon-fullscreen"
          :size="20"
          title="全屏"
          @click="onCurrenFullscreen"
        />
        <el-popover
          placement="top-end"
          trigger="click"
          transition="el-zoom-in-top"
          popper-class="table-tool-popper"
          :width="300"
          :persistent="false"
          @show="onSetTable"
        >
          <template #reference>
            <SvgIcon name="iconfont icon-quanjushezhi_o" :size="22" title="设置" />
          </template>
          <template #default>
            <div class="tool-box">
              <el-tooltip content="拖动进行排序" placement="top-start">
                <SvgIcon
                  name="fa fa-question-circle-o"
                  :size="16"
                  class="ml11"
                  color="#909399"
                />
              </el-tooltip>
              <el-checkbox
                v-model="state.checkListAll"
                :indeterminate="state.checkListIndeterminate"
                class="ml10 mr1"
                label="列显示"
                @change="onCheckAllChange"
              />
              <el-checkbox v-model="getConfig.isSerialNo" class="ml12 mr1" label="序号" />
              <el-checkbox
                v-model="getConfig.isSelection"
                class="ml12 mr1"
                label="多选"
              />
            </div>
            <el-scrollbar>
              <div ref="toolSetRef" class="tool-sortable">
                <div
                  class="tool-sortable-item"
                  v-for="v in columns"
                  :key="v.prop"
                  :data-key="v.prop"
                >
                  <i class="fa fa-arrows-alt handle cursor-pointer"></i>
                  <el-checkbox
                    v-model="v.isCheck"
                    size="default"
                    class="ml12 mr8"
                    :label="v.label"
                    @change="onCheckChange"
                  />
                </div>
              </div>
            </el-scrollbar>
          </template>
        </el-popover>
      </div>
    </header>
    <el-table
      ref="tableRef"
      v-bind="$attrs"
      v-loading="loading"
      border
      :data="tableData"
      :row-key="rowKey"
      style="width: 100%"
      @selection-change="onSelectionChange"
      @cell-dblclick="onCellDblClick"
    >
      <el-table-column
        type="selection"
        align="center"
        :reserve-selection="true"
        :resizable="false"
        width="60"
        v-if="config.isSelection"
      />
      <el-table-column
        type="index"
        align="center"
        label="序号"
        width="90"
        :index="getIndex"
        :resizable="false"
        v-if="showSerialNo"
      />
      <el-table-column
        v-for="(item, index) in setHeader"
        :key="index"
        :align="item.align ?? 'center'"
        :sortable="item.sortable"
        :width="item.width"
        :min-width="item.minWidth ?? 90"
        :label="item.label"
        :prop="item.prop"
        :fixed="item.fixed"
        :formatter="item.formatter ?? emptyFormatter"
        :resizable="index + 1 < setHeader.length"
        :reserve-selection="item.reserveSelection ?? false"
        :show-overflow-tooltip="item.showOverflowTooltip ?? true"
      >
        <!-- 自定义行slot -->
        <template v-if="item.isCustom" #default="scope">
          <slot
            :name="item.prop"
            :row="scope.row"
            :column="scope.column"
            :index="scope.$index"
          />
        </template>
        <template v-if="item.children && item.children.length > 0">
          <el-table-column
            :key="child.prop"
            v-for="child in item.children"
            v-bind="child"
          />
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="暂无数据" />
      </template>
    </el-table>
    <el-divider class="table-divider" />
    <div class="table-footer">
      <div class="table-footer-total-text flex-y-center">
        <section class="mr24">
          {{ state.page.pageSize }}项每页, 共{{ pages.total }}项
        </section>
        <section class="flex-y-center ml-30" v-if="isSelected">
          <div class="">已选择{{ selectListCount }}项</div>
          <el-divider direction="vertical" />
          <el-button type="primary" text @click.stop="onClearSelection">取消</el-button>
        </section>
      </div>
      <el-pagination
        v-model:current-page="state.page.pageIndex"
        v-model:page-size="state.page.pageSize"
        :pager-count="5"
        :page-sizes="[10, 20, 50, 100, 500]"
        :total="pages.total"
        layout="sizes, prev, pager, next, jumper"
        background
        @size-change="onHandleSizeChange"
        @current-change="onHandleCurrentChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts" name="nextTable">
import Sortable from 'sortablejs';
import mittBus from '/@utils/mitt';

import { useThemeConfig } from '/@/stores/themeConfig';
import { useSelection } from '/@hooks';

import '/@/theme/tableTool.scss';

// 默认分页
const DEFAULT_PAGE_SIZE = 10;

// 定义父组件传过来的值
const props = defineProps({
  // 列表状态
  loading: { type: Boolean, default: false },
  // 列表Key
  rowKey: {	type: String,	default: 'guid' },
	// 表头内容
  columns: { type: Array<EmptyObjectType>, default: () => [] },
  // 列表内容 
  tableData: { type: Array<EmptyObjectType>,	default: () => [] },
	// 配置项
	config: { type: Object, default: () => ({}) },
  // 分页入参
  pages: { type: Object, default: () => ({}) },
});

// 定义子组件向父组件传值/事件
const emit = defineEmits(['delRow', 'pageChange', 'sortHeader', 'refresh']);

// 定义变量内容
const route = useRoute();
const toolSetRef = ref();
const tableRef = ref();

const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);

const state = reactive({
	page: {
		pageIndex: 1,
		pageSize: DEFAULT_PAGE_SIZE,
	},
	checkListAll: true,
	checkListIndeterminate: false,
});

// 设置边框显示/隐藏
const setBorder = computed(() => !!props.config?.isBorder ?? false);
// 获取父组件 配置项(必传)
const getConfig = computed(() => props.config ?? {});
// 设置 tool header 数据
const setHeader = computed(() => props.columns.filter((v) => v.isCheck));
// 是否显示序号,默认显示
const showSerialNo = computed(() => props?.config?.isSerialNo ?? true);
// 是否显示工具栏,默认显示
const showTools = computed(() => props?.config?.isShowTools ?? true);

// 表格多选 Hooks
const { onSelectionChange, selectedList, selectListCount, isSelected } = useSelection(props.rowKey);

// 清空选中数据列表
const onClearSelection = () => tableRef.value!.clearSelection();
// 列表空数据处理
const emptyFormatter = (row, column) => {
	const { property } = column;
	return row?.[property] ?? '--';
};
// tool 列显示全选改变时
const onCheckAllChange = <T>(val: T) => {
	if (val) props.columns.forEach((v) => (v.isCheck = true));
	else props.columns.forEach((v) => (v.isCheck = false));
	state.checkListIndeterminate = false;
};
// tool 列显示当前项改变时
const onCheckChange = () => {
	const headers = props.columns.filter((v) => v.isCheck).length;
	state.checkListAll = headers === props.columns.length;
	state.checkListIndeterminate = headers > 0 && headers < props.columns.length;
};
// 双击单元格复制文本
const onCellDblClick = (row: any, column: any, cell: HTMLTableCellElement, event: Event) => {
  const target = event.target;
  if (target && target.innerText) {
    // 双击选中文本并拷贝到剪切板
    const selection = window.getSelection();
    const range = document.createRange();
    range.selectNodeContents(event.target);
    selection.removeAllRanges();
    selection.addRange(range);
    document.execCommand('Copy');
    ElMessage.success('复制成功!');
  }
};
// 删除当前项
const onDelRow = (row: EmptyObjectType) => {
	emit('delRow', row);
};
// 分页改变
const onHandleSizeChange = (size: number) => {
	state.page.pageSize = size;
	emit('pageChange', state.page);
};
// 分页改变
const onHandleCurrentChange = (page: number) => {
	state.page.pageIndex = page;
	emit('pageChange', state.page);
};
// 序号
const getIndex = (index: number) => {
  const { pageIndex, pageSize } = state.page;
  return ((pageIndex ?? 1) - 1)  * (pageSize ?? DEFAULT_PAGE_SIZE) + index + 1;
}
// 全屏
const onCurrenFullscreen = () => {
	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 4, ...route }));
};
// 刷新
const onRefreshTable = () => {
	emit('refresh', state.page);
};
// 设置
const onSetTable = () => {
	nextTick(() => {
		const sortable = Sortable.create(toolSetRef.value, {
			handle: '.handle',
			dataIdAttr: 'data-key',
			animation: 150,
			onEnd: () => {
				const headerList: EmptyObjectType[] = [];
				sortable.toArray().forEach((val: string) => {
					props.columns.forEach((v) => {
						if (v.prop === val) headerList.push({ ...v });
					});
				});
				emit('sortHeader', headerList);
			},
		});
	});
};
// 分页监听
watch(
  props.pages,
  () => {
    const { pageIndex, pageSize } = props.pages ?? {};
    state.page.pageIndex = pageIndex ?? 1;
	  state.page.pageSize = pageSize ?? DEFAULT_PAGE_SIZE;
  },
  {
    deep: true,
    immediate: true,
  }
);
</script>

<style scoped lang="scss">
.table-container {
  display: flex;
  flex-direction: column;
  flex: 1;
  .el-table {
    flex: 1;
  }
  .table-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    .table-header-tool {
      i {
        margin-right: 10px;
        cursor: pointer;
        color: var(--el-text-color-regular);
        &:last-of-type {
          margin-right: 0;
        }
      }
    }
  }
  .table-divider {
    margin: 0 0 20px 0;
  }
  .table-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    .table-footer-total-text {
      color: var(--el-text-color-regular);
      font-size: 14px;
    }
  }
}
</style>

如何使用

<template>
  <div class="layout-padding">
    <div class="form-adapt-container mb12">
      <div class="layout-padding-view layout-padding-search">
        <SearchForm
          v-model="state.searchParam"
          :columns="state.searchColumns"
          @search="onSearch"
          @reset="onReset"
        />
      </div>
    </div>
    <div class="layout-padding-auto layout-padding-view layout-padding-table">
      <CustomTable
        ref="tableRef"
        v-bind="state.tableParam"
        :loading="loading"
        :table-data="tableData"
        :pages="pages"
        @pageChange="onPageChange"
        @sortHeader="handleSortHeader"
        @refresh="onRefresh"
      >
        <template #operation-list>
          <el-button size="default" type="primary" @click="handleShow">
            <el-icon>
              <ele-FolderAdd />
            </el-icon>
            新增角色
          </el-button>
        </template>
        <template #operation="scope">
          <el-button type="primary" text @click="handleEdit(scope.row)">编辑</el-button>
          <el-divider direction="vertical" />
          <el-button type="danger" text @click="handleRemove(scope.row)">删除</el-button>
        </template>
      </CustomTable>
    </div>
    <RoleDialog
      ref="roleDialogRef"
      :permission-list="state.permissionList"
      @refresh="onRefresh"
    />
  </div>
</template>

<script setup lang="ts" name="systemRole">
import { useConfirm, useTable } from "/@/hooks";
import { getRoleListApi, removeRoleApi, getPermissionListApi } from "/@/api/system";

// 引入组件
const RoleDialog = defineAsyncComponent(
  () => import("/@/views/system/role/DialogEdit.vue")
);

// 定义变量内容
const tableRef = ref<HTMLElement>();
const roleDialogRef = ref<HTMLElement>();
const state = reactive<TableState>({
  permissionList: [], // 权限列表
  searchColumns: [
    {
      type: "input",
      label: "角色名称",
      prop: "nameKeyWord",
      placeholder: "请输入角色名称",
      span: 6,
    },
  ],
  searchParam: {
    nameKeyWord: null,
  },
  tableParam: {
    rowKey: "rowKey",
    // 表头内容(必传,注意格式)
    columns: [
      { prop: "name", label: "角色名称", isCheck: true },
      { prop: "description", label: "角色描述", isCheck: true },
      { prop: "createUserName", label: "创建人", isCheck: true },
      { prop: "createTime", label: "创建时间", isCheck: true },
      { prop: "operation", label: "操作", width: 160, isCustom: true, isCheck: true },
    ],
    // 配置项
    config: {
      isBorder: false, // 是否显示表格边框
      isSerialNo: true, // 是否显示表格序号
      isSelection: false, // 是否显示表格多选
    }
  },
});
// 列表
const {
  loading,
  tableData,
  pages,
  getTableList,
  onPageChange,
  onSearch,
  onReset,
  onRefresh,
} = useTable(getRoleListApi, state.searchParam);

// 获取用户权限
const getPermissionList = async () => {
  const { data } = await getPermissionListApi({});
  state.permissionList = data?.root?.children ?? [];
};
// 拖动显示列排序回调
const handleSortHeader = (data: TableHeaderType[]) => {
  state.tableParam.columns = data;
};
// 新增角色
const handleShow = () => {
  roleDialogRef.value.onShow();
};
// 修改角色
const handleEdit = (row: Object) => {
  roleDialogRef.value.onEdit(row);
};
// 删除角色
const handleRemove = async (row: RowRoleType) => {
  const { guid } = row ?? {};
  await useConfirm(
    removeRoleApi,
    { guid },
    `此操作将永久删除角色名称:“${row.name}”,是否继续?`
  );
  onRefresh();
};

// 页面加载时
onMounted(async () => {
  getPermissionList();
});
</script>

<style scoped lang="scss"></style>