123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- <!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 -->
- <template>
- <!-- 表格主体 -->
- <div class="pro-table-main">
- <!-- 表格头部 操作按钮 -->
- <div class="table-header">
- <div class="header-button-lf">
- <slot name="tableHeader" :selected-list="selectedList" :selected-list-ids="selectedListIds" :is-selected="isSelected" />
- </div>
- <div v-if="toolButton" class="header-button-ri">
- <slot name="toolButton">
- <el-progress :stroke-width="10" :percentage="downloadObj.percentage" v-if="downloadObj.downloading">
- <template #default="{ percentage }">
- <span class="font14">{{ percentage }}%</span>
- <span class="font14">导出进度</span>
- </template>
- </el-progress>
- <el-button v-if="showToolButton('refresh')" circle @click="onRefresh" title="刷新表格" :disabled="loading">
- <SvgIcon name="ele-Refresh" />
- </el-button>
- <el-button v-if="showToolButton('setting') && columns.length" circle @click="openColSetting" title="列设置">
- <SvgIcon name="ele-Setting" />
- </el-button>
- <el-button
- v-if="showToolButton('exportCurrent') && columns.length"
- circle
- @click="exportCurrent"
- title="导出当前页"
- :disabled="exportLoading || loading"
- >
- <SvgIcon name="iconfont icon-daochu" />
- </el-button>
- <el-button
- v-if="showToolButton('exportAll') && columns.length"
- circle
- @click="exportAll"
- title="导出全部"
- :disabled="exportLoading || loading"
- >
- <SvgIcon name="iconfont icon-export" />
- </el-button>
- <el-button v-if="exportLoading" :loading="exportLoading">导出中,请稍后。。。</el-button>
- </slot>
- <slot name="description"> </slot>
- </div>
- </div>
- <!-- 表格主体 -->
- <el-table
- ref="tableRef"
- v-bind="$attrs"
- :data="processTableData"
- :border="border"
- :row-key="rowKey"
- @selection-change="selectionChange"
- :scrollbar-always-on="true"
- v-horizontal-scroll="'always'"
- v-loading="loading"
- height="100%"
- :key="Math.random()"
- :row-style="{height: '24px'}"
- :cell-style="{padding: '4px 0'}"
- >
- <!-- 默认插槽 -->
- <slot />
- <template v-for="item in tableColumns" :key="item">
- <!-- selection || radio || index || expand || sort -->
- <el-table-column
- v-if="item.type && columnTypes.includes(item.type)"
- v-bind="item"
- :align="item.align ?? 'left'"
- :reserve-selection="item.type == 'selection'"
- >
- <template #default="scope">
- <!-- expand -->
- <template v-if="item.type == 'expand'">
- <component :is="item.render" v-bind="scope" v-if="item.render" />
- <slot v-else :name="item.type" v-bind="scope" />
- </template>
- <!-- radio -->
- <el-radio v-if="item.type == 'radio'" v-model="radio" :label="scope.row[rowKey]">
- <i></i>
- </el-radio>
- </template>
- </el-table-column>
- <!-- other -->
- <TableColumn v-if="!item.type && item.prop && item.isShow" :column="item" col-setting="">
- <template v-for="slot in Object.keys($slots)" #[slot]="scope">
- <slot :name="slot" v-bind="scope" />
- </template>
- </TableColumn>
- </template>
- <!-- 插入表格最后一行之后的插槽 -->
- <template #append>
- <slot name="append" />
- </template>
- <!-- 无数据 -->
- <template #empty>
- <el-empty :image-size="120" />
- </template>
- </el-table>
- <!-- 分页组件 -->
- <slot name="pagination">
- <PaginationEl
- v-if="pagination"
- @pagination="onRefresh"
- :total="total"
- v-model:current-page="pageIndex"
- v-model:page-size="pageSize"
- :layout="paginationLayout"
- />
- </slot>
- </div>
- <!-- 列设置 -->
- <ColSetting v-if="toolButton" ref="colRef" v-model:col-setting="colSetting" @update:colSetting="updateColSetting" @changeRow="changeRow" />
- </template>
- <script setup lang="ts" name="ProTable">
- import {ref, provide, onMounted, unref, computed, reactive, PropType, watch, nextTick} from 'vue';
- import { ElMessageBox, ElTable } from 'element-plus';
- import { useSelection } from '@/hooks/useSelection';
- import { ColumnProps, TypeProps } from '@/components/ProTable/interface';
- import PaginationEl from './components/Pagination.vue';
- import ColSetting from './components/ColSetting.vue';
- import TableColumn from './components/TableColumn.vue';
- import { downloadFileByStream } from '@/utils/tools';
- import { useResizeObserver } from '@vueuse/core'
- // 接受父组件参数,配置默认值
- const props = defineProps({
- columns: {
- // 列配置项 ==> 必传
- type: Array as PropType<ColumnProps[]>,
- required: true,
- },
- data: {
- // 静态 table data 数据
- type: Array,
- default: () => [],
- required: true,
- },
- pagination: {
- // 是否需要分页组件 ==> 非必传(默认为true)
- type: Boolean,
- default: true,
- },
- total: {
- // 分页组件的 total ==> 非必传(默认为0)
- type: Number,
- default: 0,
- },
- pageIndex: {
- // 分页组件的 currentPage ==> 非必传(默认为1)
- type: Number,
- default: 1,
- },
- pageSize: {
- // 分页组件的 pageSize ==> 非必传(默认为10)
- type: Number,
- default: 10,
- },
- border: {
- // 是否带有纵向边框 ==> 非必传(默认为false)
- type: Boolean,
- default: false,
- },
- toolButton: {
- // 是否显示表格功能按钮 ==> 非必传(默认为true)
- type: [Array, Boolean],
- // default: true,
- default: ['refresh', 'setting'],
- },
- rowKey: {
- // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ==> 非必传(默认为 id)
- type: String,
- default: 'id',
- },
- loading: {
- // 是否显示加载中效果
- type: Boolean,
- default: false,
- },
- radio: {
- // 单选框的值
- type: String,
- default: '',
- },
- paginationLayout: {
- // 分页组件布局
- type: String,
- default: 'total, sizes, prev, pager, next, jumper',
- },
- exportWithParent: {
- // 导出是否带有子父级关系
- type: Boolean,
- default: false,
- },
- exportMethod: {
- // 导出方法
- type: Function,
- default: () => {},
- },
- exportParams: {
- // 导出参数
- type: Object,
- default: () => {},
- },
- isSpecialExport: {
- // 是否特殊导出(导出传递参数不同)
- type: Boolean,
- default: false,
- },
- });
- const emit = defineEmits([
- 'dargSort',
- 'updateColSetting',
- 'selectChange',
- 'updateTable',
- 'update:pagination',
- 'exportCurrent',
- 'exportAll',
- 'update:pageSize',
- 'update:pageIndex',
- 'update:radio',
- ]);
- const pageSize = computed({
- get() {
- return props.pageSize;
- },
- set(val) {
- emit('update:pageSize', val);
- },
- });
- const pageIndex = computed({
- get() {
- return props.pageIndex;
- },
- set(val) {
- emit('update:pageIndex', val);
- },
- });
- const radio = computed({
- get() {
- return props.radio;
- },
- set(val) {
- emit('update:radio', val);
- },
- });
- // table 实例
- const tableRef = ref<InstanceType<typeof ElTable>>();
- // column 列类型
- const columnTypes: TypeProps[] = ['selection', 'radio', 'index', 'expand', 'sort'];
- // 控制 ToolButton 显示
- const showToolButton = (key: 'refresh' | 'setting' | 'exportCurrent' | 'exportAll') => {
- return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton;
- };
- // 表格多选 Hooks
- const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey);
- // 清空选中数据列表
- const clearSelection = () => tableRef.value!.clearSelection();
- // 初始化表格数据 && 拖拽排序
- onMounted(() => {
- useResizeObserver(tableRef, () => {
- tableRef.value.doLayout();
- })
- });
- // 处理表格数据
- const processTableData = computed(() => {
- if (!props.pagination) return props.data;
- return props.data;
- });
- // 监听 tableData 的变化来更新表格布局
- watch(processTableData, () => {
- if (tableRef.value) {
- tableRef.value.doLayout();
- }
- });
- // 接收 columns 并设置为响应式
- const tableColumns = ref(props.columns);
- // 扁平化 columns
- const flatColumns = computed(() => flatColumnsFunc(tableColumns.value));
- // 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
- const enumMap = ref(new Map<string, { [key: string]: any }[]>());
- const setEnumMap = async ({ prop, enum: enumValue }: ColumnProps) => {
- if (!enumValue) return;
- // 如果当前 enumMap 存在相同的值 return
- if (enumMap.value.has(prop!) && (typeof enumValue === 'function' || enumMap.value.get(prop!) === enumValue)) return;
- // 当前 enum 为静态数据,则直接存储到 enumMap
- if (typeof enumValue !== 'function') return enumMap.value.set(prop!, unref(enumValue!));
- // 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储
- enumMap.value.set(prop!, []);
- // 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap
- const { data } = await enumValue();
- enumMap.value.set(prop!, data);
- };
- // 注入 enumMap
- provide('enumMap', enumMap);
- // 扁平化 columns 的方法
- const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
- columns.forEach(async (col: any) => {
- col.name = col.label ?? '';
- if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children));
- flatArr.push(col);
- // column 添加默认 isShow
- col.isShow = col.isShow ?? true;
- // 设置 enumMap
- await setEnumMap(col);
- });
- return flatArr.filter((item) => !item._children?.length);
- };
- // 过滤需要搜索的配置项 && 排序
- const searchColumns = computed(() => {
- return flatColumns.value?.filter((item) => item.search?.el || item.search?.render).sort((a, b) => a.search!.order! - b.search!.order!);
- });
- // 设置 搜索表单默认排序 && 搜索表单项的默认值
- searchColumns.value?.forEach((column, index) => {});
- // 列设置 ==> 需要过滤掉不需要设置的列
- const colRef = ref();
- const colSetting = tableColumns.value;
- /*const colSetting: any = tableColumns!.filter((item) => {
- const { type, prop } = item;
- return !columnTypes.includes(type!) && prop !== 'operation';
- });*/
- const openColSetting = () => colRef.value.openColSetting();
- // 刷新事件
- const onRefresh = () => {
- clearSelection();
- emit('updateTable');
- };
- // 更新列设置
- const updateColSetting = (test: ColumnProps[]) => {
- // 平铺数组后获取isShow为true的对象
- const flatColumnsArr = flatColumnsFunc(test);
- const exportNewColumns = flatColumnsArr.filter((item) => item.isShow);
- // 获取prop
- const newColumnsProp = exportNewColumns.map((item) => item.prop);
- emit('updateColSetting', exportNewColumns, newColumnsProp);
- };
- // 递归获取isShow的列保持子父级关系
- const findItems = (tree: any, conditionFn: (item: any) => boolean, parent: any = null) => {
- let results: [] = [];
- for (let node: any of tree) {
- // 如果当前节点满足条件,则将其添加到结果中,并保留父级关系
- if (conditionFn(node)) {
- results.push(node);
- }
- // 递归处理当前节点的子节点
- if (node.children && node.children.length > 0) {
- results = results.concat(findItems(node.children, conditionFn, node));
- }
- }
- return results;
- };
- // 导出当前页
- const exportLoading = ref(false);
- const exportCurrent = () => {
- let exportNewColumns: any = [];
- if (props.exportWithParent) {
- // 导出带子父级关系
- exportNewColumns = findItems(tableColumns.value, (item) => item.isShow && !columnTypes.includes(item.type!) && item.prop !== 'operation');
- } else {
- exportNewColumns = flatColumnsFunc(colSetting).filter(
- (item: any) => item.isShow && !columnTypes.includes(item.type!) && item.prop !== 'operation'
- );
- }
- ElMessageBox.confirm(`您确定要导出当前页数据,是否继续?`, '提示', {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- draggable: true,
- cancelButtonClass: 'default-button',
- autofocus: false,
- })
- .then(async () => {
- exportLoading.value = true;
- const request = {
- queryDto: props.exportParams,
- columnInfos: exportNewColumns,
- isExportAll: false,
- };
- props.exportMethod &&
- props
- .exportMethod(request)
- .then((res: any) => {
- downloadFileByStream(res);
- emit('exportCurrent', exportNewColumns);
- exportLoading.value = false;
- })
- .catch((e) => {
- console.log(`导出失败:${e}`);
- exportLoading.value = false;
- });
- })
- .catch(() => {});
- };
- // 导出全部
- const exportAll = () => {
- let exportNewColumns: any = [];
- if (props.exportWithParent) {
- // 导出带子父级关系
- exportNewColumns = findItems(tableColumns.value, (item) => item.isShow && !columnTypes.includes(item.type!) && item.prop !== 'operation');
- } else {
- exportNewColumns = flatColumnsFunc(colSetting).filter(
- (item: any) => item.isShow && !columnTypes.includes(item.type!) && item.prop !== 'operation'
- );
- }
- ElMessageBox.confirm(`您确定要导出全部数据,是否继续?`, '提示', {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- draggable: true,
- cancelButtonClass: 'default-button',
- autofocus: false,
- })
- .then(() => {
- exportLoading.value = true;
- const request = {
- queryDto: props.exportParams,
- columnInfos: exportNewColumns,
- isExportAll: true,
- };
- if (props.isSpecialExport) {
- const specialRequest = {
- ...props.exportParams,
- AddColumnName: exportNewColumns.map((item: any) => item.label),
- };
- props.exportMethod &&
- props
- .exportMethod(specialRequest)
- .then((res: any) => {
- downloadFileByStream(res);
- emit('exportAll', exportNewColumns);
- exportLoading.value = false;
- })
- .catch((e) => {
- console.log(`导出失败:${e}`);
- exportLoading.value = false;
- });
- } else {
- props.exportMethod &&
- props
- .exportMethod(request)
- .then((res: any) => {
- downloadFileByStream(res);
- emit('exportAll', exportNewColumns);
- exportLoading.value = false;
- })
- .catch((e) => {
- console.log(`导出失败:${e}`);
- exportLoading.value = false;
- });
- }
- })
- .catch(() => {});
- };
- // 改变行排序
- const changeRow = ({ oldIndex, newIndex }) => {
- // 改变表头的顺序
- const draggedItem = tableColumns.value.splice(oldIndex, 1)[0];
- tableColumns.value.splice(newIndex, 0, draggedItem);
- };
- watch(
- // 监听table数据改变后重置选择
- () => props.data,
- () => {
- clearSelection();
- }
- );
- //下载逻辑
- const downloadObj = reactive({
- fileName: '',
- downloading: false,
- range: 0,
- fileBlob: [],
- percentage: 0,
- });
- const download = async () => {
- downloadObj.fileBlob = [];
- downloadObj.downloading = true;
- downloadObj.range = 0; //文件总大小
- downloadObj.percentage = 0; //下载进度
- const params = {
- md5: '7343784583fsdufhusdfgsudfe8934',
- };
- const chunkSize = 5 * 1024 * 1024;
- //第一次调接口获取到响应头的content-range,文件总大小,用于计算下载切割
- const config = {
- headers: {
- Range: `bytes=0-${chunkSize}`,
- },
- };
- const data = await downLoadbyPiece(params, config);
- //获取文件总大小
- const arr = data.headers['content-range'].split('/');
- downloadObj.range = Number(arr[1]);
- //存储每片文件流
- downloadObj.fileBlob.push(data.data);
- //获取文件名称
- let fileName = '';
- let cd = data.headers['content-disposition'];
- if (cd) {
- let index = cd.lastIndexOf('=');
- fileName = decodeURI(cd.substring(index + 1, cd.length));
- }
- await chunkUpload(params, fileName, chunkSize);
- };
- //拿到文件总大小downloadObj.range,计算分为多少都段下载
- const chunkUpload = async (params, fileName, chunkSize) => {
- //获取分段下载的数组
- let chunkList = [];
- function chunkPush(page = 1) {
- chunkList.push((page - 1) * chunkSize);
- if (page * chunkSize < downloadObj.range) {
- chunkPush(page + 1);
- }
- }
- chunkPush();
- //加上文件大小在最后一位
- chunkList.push(downloadObj.range);
- console.log(chunkList, 'chunkList');
- //分段组合传参格式处理 0-1024 1024-2048
- let uploadRange = [];
- chunkList.forEach((item, i) => {
- if (i == chunkList.length - 1) return;
- uploadRange.push(`${chunkList[i]}-${chunkList[i + 1]}`);
- });
- console.log(uploadRange, 'uploadRang');
- for (let index = 0; index < uploadRange.length; index++) {
- //第一次调接口已经传过了第一组,从第二位开始
- if (index > 0) {
- const config = {
- headers: {
- Range: `bytes=${uploadRange[index]}`,
- },
- };
- const data = await downLoadbyPiece(params, config);
- //计算下载进度
- downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100);
- emit('getDownloadpercent', downloadObj.percentage);
- //存储每一片文件流
- downloadObj.fileBlob.push(data.data);
- }
- }
- //合并
- const blob = new Blob(downloadObj.fileBlob, {
- type: downloadObj.fileBlob[0].type,
- });
- downloadObj.downloading = false;
- //下载
- const link = document.createElement('a');
- const URL = window.URL || window.webkitURL;
- link.href = URL.createObjectURL(blob);
- link.download = fileName;
- link.click();
- URL.revokeObjectURL(link.href);
- };
- // 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去)
- defineExpose({
- element: tableRef,
- tableData: processTableData,
- radio,
- clearSelection,
- enumMap,
- isSelected,
- selectedList,
- selectedListIds,
- });
- </script>
- <style scoped lang="scss">
- .pro-table-main{
- display: flex;
- flex-direction: column;
- height:calc(100% - 40px);
- .el-table{
- flex:1;
- }
- }
- .table-header {
- margin-bottom: 10px;
- display: flex;
- justify-content: space-between;
- }
- .header-button-lf {
- flex: 1;
- }
- .header-button-ri {
- }
- </style>
|