Vue3-2 - Element-Plus 二次封裝 el-table(Pro 版)
前言 📖
ProTable 組件目前已是
2.0版本
🌈,在 1.0 版本 [1] 中大家提出的問題與功能優化,目前已經得到優化和解決。
😀 歡迎大家在使用過程中發現任何問題或更好的想法,都可以在下方評論區留言,或者我的開源項目 issues 中提出。如果你覺得還不錯,請幫我點個小小的 Star 🧡
一、在線預覽 👀
Link:admin.spicyboy.cn[2]
二、Git 倉庫地址 (歡迎 Star⭐⭐⭐)
Gitee:gitee.com/laramie/Gee…[3]
GitHub:github.com/HalseySpicy…[4]
三、ProTable 功能 🚀🚀🚀
ProTable 組件目前使用屬性透傳進行重構,支持 el-table && el-table-column 所有屬性、事件、方法的調用,不會有任何心智負擔。
-
表格內容自適應屏幕寬高,溢出內容表格內部滾動(flex 佈局)
-
表格搜索、重置、分頁查詢 Hooks 封裝 (頁面使用不會存在任何搜索、重置、分頁查詢邏輯)
-
表格數據操作 Hooks 封裝 (單條數據刪除、批量刪除、重置密碼、狀態切換等操作)
-
表格數據多選 Hooks 封裝 (支持現跨頁勾選數據)
-
表格數據導入組件、導出 Hooks 封裝
-
表格搜索區域使用 Grid 佈局重構,支持自定義響應式配置
-
表格分頁組件封裝(Pagination)
-
表格數據刷新、列顯隱、列排序、搜索區域顯隱設置
-
表格數據打印功能(可勾選行數據、隱藏列打印)
-
表格配置支持多級 prop(示例 ==> prop: user.detail.name)
-
單元格內容格式化、tag 標籤顯示(有字典 enum 會根據字典 enum 自動格式化)
-
支持多級表頭、表頭內容自定義渲染(支持作用域插槽、tsx 語法、h 函數)
-
支持單元格內容自定義渲染(支持作用域插槽、tsx 語法、h 函數)
-
配合 TreeFilter、SelectFilter 組件使用更佳(項目中有使用示例)
四、ProTable 功能需求分析 📑
首先我們來看效果圖(總共可以分爲五個模塊):
-
1、表格搜索區域
-
2、表格數據操作按鈕區域
-
3、表格功能按鈕區域
-
4、表格主體內容展示區域
-
5、表格分頁區域
1、表格搜索區域需求分析:
可以看到搜索區域的字段都是存在於表格當中的,並且每個頁面的搜索、重置方法都是一樣的邏輯,只是不同的查詢參數而已。我們完全可以在傳表格配置項 columns 時,直接指定某個 column 的 search 配置,就能把該項變爲搜索項,然後使用 el 字段可以指定搜索框的類型,最後把表格的搜索方法都封裝成 Hooks 鉤子函數。頁面上完全就不會存在任何搜索、重置邏輯了。
在 1.0 版本中使用 v-if 判斷太麻煩,爲了更方便用戶傳遞參數,搜索組件在 2.0 版本中通過 component :is 動態組件 && v-bind 屬性透傳實現,將用戶傳遞的參數全部透傳到組件上,所以大家可以直接根據 element 官方文檔在 props 中傳遞參數了。以下代碼還結合了自己邏輯上的一些處理:
<template>
<component
v-if="column.search?.el"
:is="`el-${column.search.el}`"
v-bind="column.search.props"
v-model="searchParam[column.search.key ?? handleProp(column.prop!)]"
:data="column.search?.el === 'tree-select' ? columnEnum : []"
:placeholder="placeholder(column)"
:clearable="clearable(column)"
range-separator="至"
start-placeholder="開始時間"
end-placeholder="結束時間"
>
<template v-if="column.search.el === 'select'">
<component
:is="`el-option`"
v-for="(col, index) in columnEnum"
:key="index"
:label="col[fieldNames().label]"
:value="col[fieldNames().value]"
></component>
</template>
<slot v-else></slot>
</component>
</template>
<script setup lang="ts" >
import { computed, inject, ref } from "vue";
import { handleProp } from "@/utils/util";
import { ColumnProps } from "@/components/ProTable/interface";
interface SearchFormItem {
column: ColumnProps; // 具體每一個搜索項的配置
searchParam: { [key: string]: any }; // 搜索參數
}
const props = defineProps<SearchFormItem>();
// 接受 enumMap
const enumMap = inject("enumMap", ref(new Map()));
const columnEnum = computed(() => {
if (!enumMap.value.get(props.column.prop)) return [];
return enumMap.value.get(props.column.prop);
});
// 判斷 fieldNames 設置 label && value 的 key 值
const fieldNames = () => {
return {
label: props.column.fieldNames?.label ?? "label",
value: props.column.fieldNames?.value ?? "value"
};
};
// 判斷 placeholder
const placeholder = (column: ColumnProps) => {
return column.search?.props?.placeholder ?? (column.search?.el === "input" ? "請輸入" : "請選擇");
};
// 是否有清除按鈕 (當搜索項有默認值時,清除按鈕不顯示)
const clearable = (column: ColumnProps) => {
return column.search?.props?.clearable ?? (column.search?.defaultValue == null || column.search?.defaultValue == undefined);
};
</script>
複製代碼
表格搜索組件在 2.0 版本中還支持了響應式配置,使用 Grid 方法進行整體重構 😋。
2、表格數據操作按鈕區域需求分析:
表格數據操作按鈕基本上每個頁面都會不一樣,所以我們直接使用 作用域插槽 來完成每個頁面的數據操作按鈕區域,作用域插槽 可以將表格多選數據信息從 ProTable 的 Hooks 多選鉤子函數中傳到頁面上使用。
scope 數據中包含:selectedList(當前選擇的數據)、selectedListIds(當前選擇的數據 id)、isSelected(當前是否選中的數據)
<!-- ProTable 中 tableHeader 插槽 -->
<slot ></slot>
<!-- 頁面使用 -->
<template #tableHeader="scope">
<el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')">新增用戶</el-button>
<el-button type="primary" :icon="Upload" plain @click="batchAdd">批量添加用戶</el-button>
<el-button type="primary" :icon="Download" plain @click="downloadFile">導出用戶數據</el-button>
<el-button type="danger" :icon="Delete" plain @click="batchDelete(scope.selectedListIds)" :disabled="!scope.isSelected">批量刪除用戶</el-button>
</template>
複製代碼
3、表格功能按鈕區域分析:
這塊區域沒什麼特殊功能,只有四個按鈕,其功能分別爲:表格數據刷新(一直會攜帶當前查詢和分頁條件)、表格數據打印、表格列設置(列顯隱、列排序)、表格搜索區域顯隱(方便展示更多的數據信息)。可通過 toolButton 屬性控制這塊區域的顯隱。
表格打印功能基於 PrintJs 實現,因 PrintJs 不支持多級表頭打印,所以當頁面存在多級表頭時,只會打印最後一級表頭。表格打印功能可根據顯示的列和勾選的數據動態打印,默認打印當前顯示的所有數據。
4、表格主體內容展示區域分析:
🍉 該區域是最重要的數據展示區域,對於使用最多的功能就是表頭和單元格內容可以自定義渲染,在第 1.0 版本中,自定義表頭只支持傳入
renderHeader
方法,自定義單元格內容只支持slot
插槽。
💥 目前 2.0 版本中,表頭支持
headerRender
方法(避免與 el-table-column 上的屬性重名導致報錯)、作用域插槽(column.prop + 'Header'
)兩種方式自定義,單元格內容支持render
方法和作用域插槽(column 上的 prop 屬性
)兩種方式自定義。
- 使用作用域插槽:
<!-- 使用作用域插槽自定義單元格內容 username -->
<template #user>
{{ scope.row.username }}
</template>
<!-- 使用作用域插槽自定義表頭內容 username -->
<template #usernameHeader="scope">
<el-button type="primary" @click="ElMessage.success('我是通過作用域插槽渲染的表頭')">
{{ scope.row.label }}
</el-button>
</template>
複製代碼
- 使用 tsx 語法:
<script setup lang="tsx">
const columns: ColumnProps[] = [
{
prop: "username",
label: "用戶姓名",
// 使用 headerRender 自定義表頭
headerRender: (row) => {
return (
<el-button
type="primary"
onClick={() => {
ElMessage.success("我是通過 tsx 語法渲染的表頭");
}}
>
{row.label}
</el-button>
);
}
},
{
prop: "status",
label: "用戶狀態",
// 使用 render 自定義表格內容
render: (scope: { row }) => {
return (
<el-switch
model-value={scope.row.status}
active-text={scope.row.status ? "啓用" : "禁用"}
active-value={1}
inactive-value={0}
onClick={() => changeStatus(scope.row)}
/>
)
);
}
},
];
</script>
複製代碼
💢💢💢 最強大的功能:如果你想使用
el-table
的任何屬性、事件,目前通過屬性透傳都能支持。如果你還不瞭解屬性透傳,請閱讀 vue 官方文檔:cn.vuejs.org/guide/compo…[5]
-
ProTable 組件上的綁定的所有屬性和事件都會通過
v-bind="$attrs"
透傳到 el-table 上。 -
ProTable 組件內部暴露了 el-table DOM,可通過
proTable.value.element.方法名
調用其方法。
<template>
<el-table
ref="tableRef"
v-bind="$attrs"
>
</el-table>
</template>
<script setup lang="ts" >
import { ref } from "vue";
import { ElTable } from "element-plus";
const tableRef = ref<InstanceType<typeof ElTable>>();
defineExpose({ element: tableRef });
</script>
複製代碼
5、表格分頁區域分析:
分頁區域也沒有什麼特殊的功能,該支持的都支持了🤣(頁面上使用 ProTable 組件完全不存在分頁邏輯)
<template>
<!-- 分頁組件 -->
<el-pagination
:current-page="pageable.pageNum"
:page-size="pageable.pageSize"
:page-sizes="[10, 25, 50, 100]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageable.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</template>
<script setup lang="ts" >
interface Pageable {
pageNum: number;
pageSize: number;
total: number;
}
interface PaginationProps {
pageable: Pageable;
handleSizeChange: (size: number) => void;
handleCurrentChange: (currentPage: number) => void;
}
defineProps<PaginationProps>();
</script>
複製代碼
五、ProTable 文檔 📚
1、ProTable 屬性(ProTableProps):
使用
v-bind="$atts"
通過屬性透傳將 ProTable 組件屬性全部透傳到 el-table 上,所以我們支持 el-table 的所有 Props 屬性。在此基礎上,還擴展了以下 Props:
2、Column 配置(ColumnProps):
使用
v-bind="column"
通過屬性透傳將每一項 column 屬性全部透傳到 el-table-column 上,所以我們支持 el-table-column 的所有 Props 屬性。在此基礎上,還擴展了以下 Props:
3、搜索項 配置(SearchProps):
使用
v-bind="column.search.props“
通過屬性透傳將 search.props 屬性全部透傳到每一項搜索組件上,所以我們支持 input、select、tree-select、date-packer、time-picker、time-select、swicth 大部分屬性,並在其基礎上還擴展了以下 Props:
4、ProTable 事件:
根據 ElementPlus Table 文檔在 ProTable 組件上綁定事件即可,組件會通過 $attrs 透傳給 el-table。
el-table 事件文檔鏈接 [6]
5、ProTable 方法:
ProTable 組件暴露了 el-table 實例和一些組件內部的參數和方法:
el-table 方法文檔鏈接 [7]
6、ProTable 插槽:
六、代碼實現 & 基礎使用 💪(代碼較多,詳情請去項目裏查看)
使用一段話總結下我的想法:📚📚
🤔 前提:首先我們在封裝 ProTable 組件的時候,在不影響 el-table 原有的屬性、事件、方法的前提下,然後在其基礎上做二次封裝,否則做得再好,也不太完美。
🧐 思路:把一個表格頁面所有重複的功能 (表格多選、查詢、重置、刷新、分頁、數據操作二次確認、文件下載、文件上傳) 都封裝成 Hooks 函數鉤子或組件,然後在 ProTable 組件中使用這些函數鉤子或組件。在頁面中使用的時,只需傳給 ProTable 當前表格數據的請求 API、表格配置項 columns 就行了,數據傳輸都使用 作用域插槽 或 tsx 語法從 ProTable 傳遞給父組件就能在頁面上獲取到了。
1、常用 Hooks 函數
- useTable:
import { Table } from "./interface";
import { reactive, computed, onMounted, toRefs } from "vue";
/**
* @description table 頁面操作方法封裝
* @param {Function} api 獲取表格數據 api 方法(必傳)
* @param {Object} initParam 獲取數據初始化參數(非必傳,默認爲{})
* @param {Boolean} isPageable 是否有分頁(非必傳,默認爲true)
* @param {Function} dataCallBack 對後臺返回的數據進行處理的方法(非必傳)
* */
export const useTable = (
api: (params: any) => Promise<any>,
initParam: object = {},
isPageable: boolean = true,
dataCallBack?: (data: any) => any
) => {
const state = reactive<Table.TableStateProps>({
// 表格數據
tableData: [],
// 分頁數據
pageable: {
// 當前頁數
pageNum: 1,
// 每頁顯示條數
pageSize: 10,
// 總條數
total: 0,
},
// 查詢參數(只包括查詢)
searchParam: {},
// 初始化默認的查詢參數
searchInitParam: {},
// 總參數(包含分頁和查詢參數)
totalParam: {},
});
/**
* @description 分頁查詢參數(只包括分頁和表格字段排序,其他排序方式可自行配置)
* */
const pageParam = computed({
get: () => {
return {
pageNum: state.pageable.pageNum,
pageSize: state.pageable.pageSize,
};
},
set: (newVal: any) => {
console.log("我是分頁更新之後的值", newVal);
},
});
// 初始化的時候需要做的事情就是 設置表單查詢默認值 && 獲取表格數據(reset函數的作用剛好是這兩個功能)
onMounted(() => {
reset();
});
/**
* @description 獲取表格數據
* @return void
* */
const getTableList = async () => {
try {
// 先把初始化參數和分頁參數放到總參數裏面
Object.assign(
state.totalParam,
initParam,
isPageable ? pageParam.value : {}
);
let { data } = await api(state.totalParam);
dataCallBack && (data = dataCallBack(data));
state.tableData = isPageable ? data.datalist : data;
// 解構後臺返回的分頁數據 (如果有分頁更新分頁信息)
const { pageNum, pageSize, total } = data;
isPageable && updatePageable({ pageNum, pageSize, total });
} catch (error) {
console.log(error);
}
};
/**
* @description 更新查詢參數
* @return void
* */
const updatedTotalParam = () => {
state.totalParam = {};
// 處理查詢參數,可以給查詢參數加自定義前綴操作
let nowSearchParam: { [key: string]: any } = {};
// 防止手動清空輸入框攜帶參數(這裏可以自定義查詢參數前綴)
for (let key in state.searchParam) {
// * 某些情況下參數爲 false/0 也應該攜帶參數
if (
state.searchParam[key] ||
state.searchParam[key] === false ||
state.searchParam[key] === 0
) {
nowSearchParam[key] = state.searchParam[key];
}
}
Object.assign(
state.totalParam,
nowSearchParam,
isPageable ? pageParam.value : {}
);
};
/**
* @description 更新分頁信息
* @param {Object} resPageable 後臺返回的分頁數據
* @return void
* */
const updatePageable = (resPageable: Table.Pageable) => {
Object.assign(state.pageable, resPageable);
};
/**
* @description 表格數據查詢
* @return void
* */
const search = () => {
state.pageable.pageNum = 1;
updatedTotalParam();
getTableList();
};
/**
* @description 表格數據重置
* @return void
* */
const reset = () => {
state.pageable.pageNum = 1;
state.searchParam = {};
// 重置搜索表單的時,如果有默認搜索參數,則重置默認的搜索參數
Object.keys(state.searchInitParam).forEach((key) => {
state.searchParam[key] = state.searchInitParam[key];
});
updatedTotalParam();
getTableList();
};
/**
* @description 每頁條數改變
* @param {Number} val 當前條數
* @return void
* */
const handleSizeChange = (val: number) => {
state.pageable.pageNum = 1;
state.pageable.pageSize = val;
getTableList();
};
/**
* @description 當前頁改變
* @param {Number} val 當前頁
* @return void
* */
const handleCurrentChange = (val: number) => {
state.pageable.pageNum = val;
getTableList();
};
return {
...toRefs(state),
getTableList,
search,
reset,
handleSizeChange,
handleCurrentChange,
};
};
複製代碼
- useSelection:
import { ref, computed } from "vue";
/**
* @description 表格多選數據操作
* @param {String} selectId 當表格可以多選時,所指定的 id
* @param {Any} tableRef 當表格 ref
* */
export const useSelection = (selectId: string = "id") => {
// 是否選中數據
const isSelected = ref<boolean>(false);
// 選中的數據列表
const selectedList = ref([]);
// 當前選中的所有ids(數組),可根據項目自行配置id字段
const selectedListIds = computed((): string[] => {
let ids: string[] = [];
selectedList.value.forEach(item => {
ids.push(item[selectId]);
});
return ids;
});
// 獲取行數據的 Key,用來優化 Table 的渲染;在使用跨頁多選時,該屬性是必填的
const getRowKeys = (row: any) => {
return row[selectId];
};
/**
* @description 多選操作
* @param {Array} rowArr 當前選擇的所有數據
* @return void
*/
const selectionChange = (rowArr: any) => {
rowArr.length === 0 ? (isSelected.value = false) : (isSelected.value = true);
selectedList.value = rowArr;
};
return {
isSelected,
selectedList,
selectedListIds,
selectionChange,
getRowKeys
};
};
複製代碼
- useDownload:
import { ElNotification } from "element-plus";
/**
* @description 接收數據流生成blob,創建鏈接,下載文件
* @param {Function} api 導出表格的api方法(必傳)
* @param {String} tempName 導出的文件名(必傳)
* @param {Object} params 導出的參數(默認爲空對象)
* @param {Boolean} isNotify 是否有導出消息提示(默認爲 true)
* @param {String} fileType 導出的文件格式(默認爲.xlsx)
* @return void
* */
export const useDownload = async (
api: (param: any) => Promise<any>,
tempName: string,
params: any = {},
isNotify: boolean = true,
fileType: string = ".xlsx"
) => {
if (isNotify) {
ElNotification({
title: "溫馨提示",
message: "如果數據龐大會導致下載緩慢哦,請您耐心等待!",
type: "info",
duration: 3000
});
}
try {
const res = await api(params);
const blob = new Blob([res]);
// 兼容 edge 不支持 createObjectURL 方法
if ("msSaveOrOpenBlob" in navigator) return window.navigator.msSaveOrOpenBlob(blob, tempName + fileType);
const blobUrl = window.URL.createObjectURL(blob);
const exportFile = document.createElement("a");
exportFile.style.display = "none";
exportFile.download = `${tempName}${fileType}`;
exportFile.href = blobUrl;
document.body.appendChild(exportFile);
exportFile.click();
// 去除下載對 url 的影響
document.body.removeChild(exportFile);
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.log(error);
}
};
複製代碼
- useHandleData:
import { ElMessageBox, ElMessage } from "element-plus";
import { HandleData } from "./interface";
/**
* @description 操作單條數據信息(二次確認【刪除、禁用、啓用、重置密碼】)
* @param {Function} api 操作數據接口的api方法(必傳)
* @param {Object} params 攜帶的操作數據參數 {id,params}(必傳)
* @param {String} message 提示信息(必傳)
* @param {String} confirmType icon類型(不必傳,默認爲 warning)
* @return Promise
*/
export const useHandleData = <P = any, R = any>(
api: (params: P) => Promise<R>,
params: Parameters<typeof api>[0],
message: string,
confirmType: HandleData.MessageType = "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: `${message}成功!`
});
resolve(true);
});
});
};
複製代碼
2、Protable 組件:
- ProTable:
<template>
<!-- 查詢表單 card -->
<SearchForm
:search="search"
:reset="reset"
:searchParam="searchParam"
:columns="searchColumns"
:searchCol="searchCol"
v-show="isShowSearch"
/>
<!-- 表格內容 card -->
<div class="card table">
<!-- 表格頭部 操作按鈕 -->
<div class="table-header">
<div class="header-button-lf">
<slot ></slot>
</div>
<div class="header-button-ri" v-if="toolButton">
<el-button :icon="Refresh" circle @click="getTableList"> </el-button>
<el-button :icon="Printer" circle v-if="columns.length" @click="handlePrint"> </el-button>
<el-button :icon="Operation" circle v-if="columns.length" @click="openColSetting"> </el-button>
<el-button :icon="Search" circle v-if="searchColumns.length" @click="isShowSearch = !isShowSearch"> </el-button>
</div>
</div>
<!-- 表格主體 -->
<el-table
ref="tableRef"
v-bind="$attrs"
:data="tableData"
:border="border"
:row-key="getRowKeys"
@selection-change="selectionChange"
>
<!-- 默認插槽 -->
<slot></slot>
<template v-for="item in tableColumns" :key="item">
<!-- selection || index -->
<el-table-column
v-bind="item"
:align="item.align ?? 'center'"
:reserve-selection="item.type == 'selection'"
v-if="item.type == 'selection' || item.type == 'index'"
>
</el-table-column>
<!-- expand 支持 tsx 語法 && 作用域插槽 (tsx > slot) -->
<el-table-column v-bind="item" :align="item.align ?? 'center'" v-if="item.type == 'expand'" v-slot="scope">
<component :is="item.render" :row="scope.row" v-if="item.render"> </component>
<slot : v-else></slot>
</el-table-column>
<!-- other 循環遞歸 -->
<TableColumn v-if="!item.type && item.prop && item.isShow" :column="item">
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
<slot :></slot>
</template>
</TableColumn>
</template>
<!-- 無數據 -->
<template #empty>
<div class="table-empty">
<img src="@/assets/images/notData.png" alt="notData" />
<div>暫無數據</div>
</div>
</template>
</el-table>
<!-- 分頁組件 -->
<Pagination
v-if="pagination"
:pageable="pageable"
:handleSizeChange="handleSizeChange"
:handleCurrentChange="handleCurrentChange"
/>
</div>
<!-- 列設置 -->
<ColSetting v-if="toolButton" ref="colRef" v-model:colSetting="colSetting" />
</template>
<script setup lang="ts" >
import { ref, watch, computed, provide } from "vue";
import { useTable } from "@/hooks/useTable";
import { useSelection } from "@/hooks/useSelection";
import { BreakPoint } from "@/components/Grid/interface";
import { ColumnProps } from "@/components/ProTable/interface";
import { ElTable, TableProps } from "element-plus";
import { Refresh, Printer, Operation, Search } from "@element-plus/icons-vue";
import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from "@/utils/util";
import SearchForm from "@/components/SearchForm/index.vue";
import Pagination from "./components/Pagination.vue";
import ColSetting from "./components/ColSetting.vue";
import TableColumn from "./components/TableColumn.vue";
import printJS from "print-js";
// 表格 DOM 元素
const tableRef = ref<InstanceType<typeof ElTable>>();
// 是否顯示搜索模塊
const isShowSearch = ref<boolean>(true);
interface ProTableProps extends Partial<Omit<TableProps<any>, "data">> {
columns: ColumnProps[]; // 列配置項
requestApi: (params: any) => Promise<any>; // 請求表格數據的api ==> 必傳
dataCallback?: (data: any) => any; // 返回數據的回調函數,可以對數據進行處理 ==> 非必傳
title?: string; // 表格標題,目前只在打印的時候用到 ==> 非必傳
pagination?: boolean; // 是否需要分頁組件 ==> 非必傳(默認爲true)
initParam?: any; // 初始化請求參數 ==> 非必傳(默認爲{})
border?: boolean; // 是否帶有縱向邊框 ==> 非必傳(默認爲true)
toolButton?: boolean; // 是否顯示錶格功能按鈕 ==> 非必傳(默認爲true)
selectId?: string; // 當表格數據多選時,所指定的 id ==> 非必傳(默認爲 id)
searchCol?: number | Record<BreakPoint, number>; // 表格搜索項 每列佔比配置 ==> 非必傳 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
}
// 接受父組件參數,配置默認值
const props = withDefaults(defineProps<ProTableProps>(), {
columns: () => [],
pagination: true,
initParam: {},
border: true,
toolButton: true,
selectId: "id",
searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 })
});
// 表格多選 Hooks
const { selectionChange, getRowKeys, selectedList, selectedListIds, isSelected } = useSelection(props.selectId);
// 表格操作 Hooks
const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange } =
useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback);
// 清空選中數據列表
const clearSelection = () => tableRef.value!.clearSelection();
// 監聽頁面 initParam 改化,重新獲取表格數據
watch(
() => props.initParam,
() => {
getTableList();
},
{ deep: true }
);
// 接收 columns 並設置爲響應式
const tableColumns = ref<ColumnProps[]>(props.columns);
// 定義 enumMap 存儲 enum 值(避免異步請求無法格式化單元格內容 || 無法填充搜索下拉選擇)
const enumMap = ref(new Map<string, { [key: string]: any }[]>());
provide("enumMap", enumMap);
// 扁平化 columns && 處理 tableColumns 數據
const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
columns.forEach(async col => {
if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children));
flatArr.push(col);
// 給每一項 column 添加 isShow && isFilterEnum 屬性
col.isShow = col.isShow ?? true;
col.isFilterEnum = col.isFilterEnum ?? true;
if (!col.enum) return;
// 如果當前 enum 爲後臺數據需要請求數據,則調用該請求接口,並存儲到 enumMap
if (typeof col.enum !== "function") return enumMap.value.set(col.prop!, col.enum);
const { data } = await col.enum();
enumMap.value.set(col.prop!, data);
});
return flatArr.filter(item => !item._children?.length);
};
// 扁平 columns
const flatColumns = ref<ColumnProps[]>();
flatColumns.value = flatColumnsFunc(tableColumns.value as any);
// 過濾需要搜索的配置項 && 處理搜索排序
const searchColumns = flatColumns.value
.filter(item => item.search?.el)
.sort((a, b) => (b.search?.order ?? 0) - (a.search?.order ?? 0));
// 設置搜索表單的默認值
searchColumns.forEach(column => {
if (column.search?.defaultValue !== undefined && column.search?.defaultValue !== null) {
searchInitParam.value[column.search.key ?? handleProp(column.prop!)] = column.search?.defaultValue;
}
});
// 列設置 ==> 過濾掉不需要設置顯隱的列
const colRef = ref();
const colSetting = tableColumns.value!.filter(item => {
return item.isShow && item.type !== "selection" && item.type !== "index" && item.type !== "expand" && item.prop !== "operation";
});
const openColSetting = () => {
colRef.value.openColSetting();
};
// 處理打印數據(把後臺返回的值根據 enum 做轉換)
const printData = computed(() => {
let printDataList = JSON.parse(JSON.stringify(selectedList.value.length ? selectedList.value : tableData.value));
let colEnumList = flatColumns.value!.filter(item => item.enum || (item.prop && item.prop.split(".").length > 1));
colEnumList.forEach(colItem => {
printDataList.forEach((tableItem: { [key: string]: any }) => {
tableItem[handleProp(colItem.prop!)] =
colItem.prop!.split(".").length > 1 && !colItem.enum
? formatValue(handleRowAccordingToProp(tableItem, colItem.prop!))
: filterEnum(handleRowAccordingToProp(tableItem, colItem.prop!), enumMap.value.get(colItem.prop!), colItem.fieldNames);
});
});
return printDataList;
});
// 打印表格數據(💥 多級表頭數據打印時,只能扁平化成一維數組,printJs 不支持多級表頭打印)
const handlePrint = () => {
printJS({
printable: printData.value,
header: props.title && `<div style="display: flex;flex-direction: column;text-align: center"><h2>${props.title}</h2></div>`,
properties: flatColumns
.value!.filter(
item =>
item.isShow && item.type !== "selection" && item.type !== "index" && item.type !== "expand" && item.prop !== "operation"
)
.map((item: ColumnProps) => {
return {
field: handleProp(item.prop!),
displayName: item.label
};
}),
type: "json",
gridHeaderStyle:
"border: 1px solid #ebeef5;height: 45px;font-size: 14px;color: #232425;text-align: center;background-color: #fafafa;",
gridStyle: "border: 1px solid #ebeef5;height: 40px;font-size: 14px;color: #494b4e;text-align: center"
});
};
// 暴露給父組件的參數和方法(外部需要什麼,都可以從這裏暴露出去)
defineExpose({ element: tableRef, tableData, searchParam, pageable, getTableList, clearSelection });
</script>
複製代碼
- TableColumn:
<template>
<component :is="renderLoop(column)"></component>
</template>
<script lang="tsx" setup>
import { inject, ref, useSlots } from "vue";
import { ElTableColumn, ElTag } from "element-plus";
import { filterEnum, formatValue, handleRowAccordingToProp } from "@/utils/util";
import { ColumnProps } from "@/components/ProTable/interface";
const slots = useSlots();
defineProps<{ column: ColumnProps }>();
const enumMap = inject("enumMap", ref(new Map()));
// 渲染表格數據
const renderCellData = (item: ColumnProps, scope: { [key: string]: any }) => {
return enumMap.value.get(item.prop) && item.isFilterEnum
? filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop)!, item.fieldNames)
: formatValue(handleRowAccordingToProp(scope.row, item.prop!));
};
// 獲取 tag 類型
const getTagType = (item: ColumnProps, scope: { [key: string]: any }) => {
return filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames, "tag") as any;
};
const renderLoop = (item: ColumnProps) => {
return (
<>
{item.isShow && (
<ElTableColumn
{...item}
align={item.align ?? "center"}
showOverflowTooltip={item.showOverflowTooltip ?? item.prop !== "operation"}
>
{{
default: (scope: any) => {
if (item._children) return item._children.map(child => renderLoop(child));
if (item.render) return item.render(scope);
if (slots[item.prop!]) return slots[item.prop!]!(scope);
if (item.tag) return <ElTag type={getTagType(item, scope)}>{renderCellData(item, scope)}</ElTag>;
return renderCellData(item, scope);
},
header: () => {
if (item.headerRender) return item.headerRender(item);
if (slots[`${item.prop}Header`]) return slots[`${item.prop}Header`]!({ row: item });
return item.label;
}
}}
</ElTableColumn>
)}
</>
);
};
</script>
複製代碼
3、頁面使用 ProTable 組件:
<template>
<div class="table-box">
<ProTable
ref="proTable"
title="用戶列表"
:columns="columns"
:requestApi="getTableList"
:initParam="initParam"
:dataCallback="dataCallback"
>
<!-- 表格 header 按鈕 -->
<template #tableHeader="scope">
<el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')" v-auth="['add']">新增用戶</el-button>
<el-button type="primary" :icon="Upload" plain @click="batchAdd" v-auth="['batchAdd']">批量添加用戶</el-button>
<el-button type="primary" :icon="Download" plain @click="downloadFile" v-auth="['export']">導出用戶數據</el-button>
<el-button type="danger" :icon="Delete" plain @click="batchDelete(scope.selectedListIds)" :disabled="!scope.isSelected">
批量刪除用戶
</el-button>
</template>
<!-- Expand -->
<template #expand="scope">
{{ scope.row }}
</template>
<!-- usernameHeader -->
<template #usernameHeader="scope">
<el-button type="primary" @click="ElMessage.success('我是通過作用域插槽渲染的表頭')">
{{ scope.row.label }}
</el-button>
</template>
<!-- createTime -->
<template #createTime="scope">
<el-button type="primary" link @click="ElMessage.success('我是通過作用域插槽渲染的內容')">
{{ scope.row.createTime }}
</el-button>
</template>
<!-- 表格操作 -->
<template #operation="scope">
<el-button type="primary" link :icon="View" @click="openDrawer('查看', scope.row)">查看</el-button>
<el-button type="primary" link :icon="EditPen" @click="openDrawer('編輯', scope.row)">編輯</el-button>
<el-button type="primary" link :icon="Refresh" @click="resetPass(scope.row)">重置密碼</el-button>
<el-button type="primary" link :icon="Delete" @click="deleteAccount(scope.row)">刪除</el-button>
</template>
</ProTable>
<UserDrawer ref="drawerRef" />
<ImportExcel ref="dialogRef" />
</div>
</template>
<script setup lang="tsx" >
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { User } from "@/api/interface";
import { ColumnProps } from "@/components/ProTable/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { useDownload } from "@/hooks/useDownload";
import { useAuthButtons } from "@/hooks/useAuthButtons";
import ProTable from "@/components/ProTable/index.vue";
import ImportExcel from "@/components/ImportExcel/index.vue";
import UserDrawer from "@/views/proTable/components/UserDrawer.vue";
import { CirclePlus, Delete, EditPen, Download, Upload, View, Refresh } from "@element-plus/icons-vue";
import {
getUserList,
deleteUser,
editUser,
addUser,
changeUserStatus,
resetUserPassWord,
exportUserInfo,
BatchAddUser,
getUserStatus,
getUserGender
} from "@/api/modules/user";
// 獲取 ProTable 元素,調用其獲取刷新數據方法(還能獲取到當前查詢參數,方便導出攜帶參數)
const proTable = ref();
// 如果表格需要初始化請求參數,直接定義傳給 ProTable(之後每次請求都會自動帶上該參數,此參數更改之後也會一直帶上,改變此參數會自動刷新表格數據)
const initParam = reactive({
type: 1
});
// dataCallback 是對於返回的表格數據做處理,如果你後臺返回的數據不是 datalist && total && pageNum && pageSize 這些字段,那麼你可以在這裏進行處理成這些字段
const dataCallback = (data: any) => {
return {
datalist: data.datalist,
total: data.total,
pageNum: data.pageNum,
pageSize: data.pageSize
};
};
// 如果你想在請求之前對當前請求參數做一些操作,可以自定義如下函數:params 爲當前所有的請求參數(包括分頁),最後返回請求列表接口
// 默認不做操作就直接在 ProTable 組件上綁定 :requestApi="getUserList"
const getTableList = (params: any) => {
let newParams = { ...params };
newParams.username && (newParams.username = "custom-" + newParams.username);
return getUserList(newParams);
};
// 頁面按鈕權限(按鈕權限既可以使用 hooks,也可以直接使用 v-auth 指令,指令適合直接綁定在按鈕上,hooks 適合根據按鈕權限顯示不同的內容)
const { BUTTONS } = useAuthButtons();
// 自定義渲染表頭(使用tsx語法)
const headerRender = (row: ColumnProps) => {
return (
<el-button
type="primary"
onClick={() => {
ElMessage.success("我是通過 tsx 語法渲染的表頭");
}}
>
{row.label}
</el-button>
);
};
// 表格配置項
const columns: ColumnProps[] = [
{ type: "selection", fixed: "left", width: 80 },
{ type: "index", label: "#", width: 80 },
{ type: "expand", label: "Expand", width: 100 },
{
prop: "username",
label: "用戶姓名",
search: { el: "input" },
render: scope => {
return (
<el-button type="primary" link onClick={() => ElMessage.success("我是通過 tsx 語法渲染的內容")}>
{scope.row.username}
</el-button>
);
}
},
{
prop: "gender",
label: "性別",
enum: getUserGender,
fieldNames: { label: "genderLabel", value: "genderValue" },
search: { el: "select" }
},
// 多級 prop
{ prop: "user.detail.age", label: "年齡", search: { el: "input" } },
{ prop: "idCard", label: "身份證號", search: { el: "input" } },
{ prop: "email", label: "郵箱", search: { el: "input" } },
{ prop: "address", label: "居住地址" },
{
prop: "status",
label: "用戶狀態",
enum: getUserStatus,
fieldNames: { label: "userLabel", value: "userStatus" },
search: {
el: "tree-select",
props: { props: { label: "userLabel" }, nodeKey: "userStatus" }
},
render: (scope: { row: User.ResUserList }) => {
return (
<>
{BUTTONS.value.status ? (
<el-switch
model-value={scope.row.status}
active-text={scope.row.status ? "啓用" : "禁用"}
active-value={1}
inactive-value={0}
onClick={() => changeStatus(scope.row)}
/>
) : (
<el-tag type={scope.row.status ? "success" : "danger"}>{scope.row.status ? "啓用" : "禁用"}</el-tag>
)}
</>
);
}
},
{
prop: "createTime",
label: "創建時間",
headerRender,
width: 200,
search: {
el: "date-picker",
span: 2,
defaultValue: ["2022-11-12 11:35:00", "2022-12-12 11:35:00"],
props: { type: "datetimerange" }
}
},
{ prop: "operation", label: "操作", fixed: "right", width: 330 }
];
// 刪除用戶信息
const deleteAccount = async (params: User.ResUserList) => {
await useHandleData(deleteUser, { id: [params.id] }, `刪除【${params.username}】用戶`);
proTable.value.getTableList();
};
// 批量刪除用戶信息
const batchDelete = async (id: string[]) => {
await useHandleData(deleteUser, { id }, "刪除所選用戶信息");
proTable.value.clearSelection();
proTable.value.getTableList();
};
// 重置用戶密碼
const resetPass = async (params: User.ResUserList) => {
await useHandleData(resetUserPassWord, { id: params.id }, `重置【${params.username}】用戶密碼`);
proTable.value.getTableList();
};
// 切換用戶狀態
const changeStatus = async (row: User.ResUserList) => {
await useHandleData(changeUserStatus, { id: row.id, status: row.status == 1 ? 0 : 1 }, `切換【${row.username}】用戶狀態`);
proTable.value.getTableList();
};
// 導出用戶列表
const downloadFile = async () => {
useDownload(exportUserInfo, "用戶列表", proTable.value.searchParam);
};
// 批量添加用戶
const dialogRef = ref();
const batchAdd = () => {
let params = {
title: "用戶",
tempApi: exportUserInfo,
importApi: BatchAddUser,
getTableList: proTable.value.getTableList
};
dialogRef.value.acceptParams(params);
};
// 打開 drawer(新增、查看、編輯)
const drawerRef = ref();
const openDrawer = (title: string, rowData: Partial<User.ResUserList> = {}) => {
let params = {
title,
rowData: { ...rowData },
isView: title === "查看",
api: title === "新增" ? addUser : title === "編輯" ? editUser : "",
getTableList: proTable.value.getTableList
};
drawerRef.value.acceptParams(params);
};
</script>
複製代碼
七、貢獻者 👨👦👦
-
HalseySpicy[8]
-
denganjia[9]
關於本文
來自:SpicyBoy
https://juejin.cn/post/7166068828202336263
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/B-v6hyBLLDrFNuF5pXfBoQ