2024-03-18 17:17:49 +08:00

286 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 -->
<template>
<!-- 查询表单 card -->
<SearchForm
:search="search"
:reset="reset"
:onReset="onReset"
:searchParam="searchParam"
:columns="searchColumns"
:searchCol="searchCol"
v-show="isShowSearch"
>
<template #formButton>
<slot name="formButton" :selectedListIds="selectedListIds" :selectedList="selectedList" :isSelected="isSelected"></slot>
</template>
</SearchForm>
<!-- 表格内容 card -->
<div class="card table-main">
<!-- 表格头部 操作按钮 -->
<div class="table-header">
<div class="header-button-lf">
<slot name="tableHeader" :selectedListIds="selectedListIds" :selectedList="selectedList" :isSelected="isSelected"></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>
<!-- 表格主体 -->
<!-- :border="border" -->
<el-table
ref="tableRef"
v-bind="$attrs"
:data="tableData"
:stripe="stripe"
:row-key="getRowKeys"
@selection-change="selectionChange"
:header-cell-style="{ fontWeight: 500, backgroundColor: 'transparent' }"
:cell-style="{ fontSize: '12px' }"
class="protable"
>
<!-- 默认插槽 -->
<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>
<!-- 富文本 -->
<el-table-column v-bind="item" :align="item.align ?? 'center'" v-if="item.type == 'html'" v-slot="scope">
<component :is="item.render" :row="scope.row" v-if="item.render"> </component>
<slot :name="item.type" :row="scope.row" v-else></slot>
</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 :name="item.type" :row="scope.row" 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 :name="slot" :row="scope.row" :index="scope.$index"></slot>
</template>
</TableColumn>
</template>
<!-- 插入表格最后一行之后的插槽 -->
<template #append>
<slot name="append"> </slot>
</template>
<!-- 表格无数据情况 -->
<template #empty>
<div class="table-empty">
<slot name="empty">
<img src="@/assets/images/notData.png" alt="notData" />
<div>暂无数据</div>
</slot>
</div>
</template>
</el-table>
<!-- 分页组件 -->
<slot name="pagination">
<Pagination
v-if="pagination"
:pageable="pageable"
:handleSizeChange="handleSizeChange"
:handleCurrentChange="handleCurrentChange"
:background="background"
/>
</slot>
<changeTheme />
</div>
<!-- 列设置 -->
<ColSetting v-if="toolButton" ref="colRef" v-model:colSetting="colSetting" />
</template>
<script setup lang="ts" name="ProTable">
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";
import changeTheme from "@/components/changeTheme/index.vue";
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>;
addButton?: boolean; // 是否显示增加按钮 // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
background?: boolean; // 是否分页背景颜色
onReset?: boolean;
stripe?: boolean;
}
// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<ProTableProps>(), {
columns: () => [],
pagination: true,
initParam: {},
border: false,
toolButton: true,
stripe: true,
selectId: "id",
searchCol: () => ({ xs: 1, sm: 2, md: 4, lg: 5, xl: 5 })
});
// 是否显示搜索模块
const isShowSearch = ref(true);
// 表格 DOM 元素
const tableRef = ref<InstanceType<typeof ElTable>>();
// 表格多选 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);
const setEnumMap = async (col: ColumnProps) => {
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);
};
// 扁平化 columns
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;
// 设置 enumMap
setEnumMap(col);
});
return flatArr.filter(item => !item._children?.length);
};
// flatColumns
const flatColumns = ref<ColumnProps[]>();
flatColumns.value = flatColumnsFunc(tableColumns.value);
// 过滤需要搜索的配置项
const searchColumns = flatColumns.value.filter(item => item.search?.el);
// 设置搜索表单排序默认值 && 设置搜索表单项的默认值
searchColumns.forEach((column, index) => {
column.search!.order = column.search!.order ?? index + 2;
if (column.search?.defaultValue !== undefined && column.search?.defaultValue !== null) {
searchInitParam.value[column.search.key ?? handleProp(column.prop!)] = column.search?.defaultValue;
}
});
// 排序搜索表单项
searchColumns.sort((a, b) => a.search!.order! - b.search!.order!);
// 列设置 ==> 过滤掉不需要设置显隐的列
const colRef = ref();
const colSetting = tableColumns.value!.filter(item => {
return 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));
// 找出需要转换数据的列(有 enum || 多级 prop && 需要根据 enum 格式化)
let needTransformCol = flatColumns.value!.filter(
item => (item.enum || (item.prop && item.prop.split(".").length > 1)) && item.isFilterEnum
);
needTransformCol.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);
for (const key in tableItem) {
if (tableItem[key] === null) tableItem[key] = formatValue(tableItem[key]);
}
});
});
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) => ({ field: handleProp(item.prop!), displayName: item.label })),
type: "json",
gridHeaderStyle:
"border: 1px solid #ebeef5;height: 45px;font-size: 20px;color: #232425;text-align: center;background-color: #fafafa;",
gridStyle: "border: 1px solid #ebeef5;height: 40px;font-size: 20px;color: #494b4e;text-align: center"
});
};
// 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去)
defineExpose({
element: tableRef,
tableData,
searchParam,
pageable,
getTableList,
reset,
clearSelection,
enumMap,
isSelected,
selectedList,
selectedListIds
});
</script>
<style lang="scss" scoped>
:deep(.el-table__inner-wrapper::before) {
background-color: #fff;
}
:deep(.el-table td.el-table__cell, .el-table th.el-table__cell.is-leaf) {
border-bottom: none;
}
</style>