Browse Source

feat: 新增导出功能

qq12rrr 1 tháng trước cách đây
mục cha
commit
92d5e4fb28

+ 8 - 0
src/components/ExportView/index.ts

@@ -0,0 +1,8 @@
+import { withInstall } from "@pureadmin/utils";
+import exportView from "./index.vue";
+
+/** 导出组件 */
+const ExportView = withInstall(exportView);
+
+export { ExportView };
+export default ExportView;

+ 323 - 0
src/components/ExportView/index.vue

@@ -0,0 +1,323 @@
+<template>
+  <el-button plain size="small" @click="internalVisible = true">
+    导出数据
+  </el-button>
+  <el-dialog
+    center
+    :close-on-click-modal="false"
+    destroy-on-close
+    v-model="internalVisible"
+    :title="dialogTitle"
+    width="480px"
+    :before-close="handleClose"
+  >
+    <el-form ref="formRef" :model="form" :rules="rules" @submit.prevent>
+      <el-row justify="center">
+        <el-form-item prop="startPage">
+          <el-input
+            v-model="form.startPage"
+            placeholder="请输入开始页码"
+            clearable
+            @keyup.enter="handleExport"
+            style="max-width: 120px"
+          />
+        </el-form-item>
+
+        <span style="margin: 5px 10px 0">至</span>
+
+        <el-form-item prop="endPage">
+          <el-input
+            v-model="form.endPage"
+            placeholder="请输入结束页码"
+            clearable
+            @keyup.enter="handleExport"
+            style="max-width: 120px"
+          />
+        </el-form-item>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleExport" :loading="loading">
+          导出
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from "vue";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage } from "element-plus";
+import dayjs from "dayjs";
+
+import { http } from "@/utils/http/index";
+
+// 定义接口
+interface ExportForm {
+  startPage: any;
+  endPage: any;
+}
+
+// Props 定义
+const props = withDefaults(
+  defineProps<{
+    title?: string;
+    url: string;
+    fileName: string;
+    queryParams?: any;
+    likeTable?: boolean;
+  }>(),
+  {
+    title: "导出数据",
+    likeTable: false
+  }
+);
+
+// 响应式数据
+const internalVisible = ref(false);
+const loading = ref(false);
+const formRef = ref<FormInstance>();
+
+// 计算属性
+const dialogTitle = computed(() => props.title);
+
+// 表单数据
+const form = reactive<ExportForm>({
+  startPage: "1",
+  endPage: "10"
+});
+
+/**
+ * @description 从1开始正整数校验
+ * @param val
+ * @returns {boolean}
+ */
+const isNumbAvailable = (val: any): boolean => {
+  let nubReg = /^[1-9]\d*$/;
+  return nubReg.test(val);
+};
+
+// 自定义验证函数
+const validatePageNumber = (rule: any, value: string, callback: any) => {
+  if (!value) {
+    callback(new Error("请输入页码"));
+    return;
+  }
+
+  if (!isNumbAvailable(value)) {
+    callback(new Error("请输入有效的正整数"));
+    return;
+  }
+
+  callback();
+};
+
+const validateEndPage = (rule: any, value: string, callback: any) => {
+  if (!value) {
+    callback(new Error("请输入结束页码"));
+    return;
+  }
+
+  if (!isNumbAvailable(value)) {
+    callback(new Error("请输入有效的正整数"));
+    return;
+  }
+
+  const startPageNum = parseInt(form.startPage);
+  const endPageNum = parseInt(value);
+
+  if (endPageNum < startPageNum) {
+    callback(new Error("结束页不能小于开始页"));
+    return;
+  }
+
+  callback();
+};
+
+// 表单验证规则
+const rules = reactive<FormRules<ExportForm>>({
+  startPage: [{ validator: validatePageNumber, trigger: "blur" }],
+  endPage: [{ validator: validateEndPage, trigger: "blur" }]
+});
+
+/**
+ * 下载文件
+ * @param blob 文件blob对象
+ * @param fileName 文件名
+ */
+const downloadFile = (blob: Blob, fileName: string) => {
+  try {
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = fileName;
+    link.style.display = "none";
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+  } catch (error) {
+    console.error("文件下载失败:", error);
+    ElMessage.error("文件下载失败");
+  }
+};
+
+/**
+ * 生成文件名
+ * @param baseName 基础文件名
+ * @param extension 文件扩展名
+ * @returns 完整文件名
+ */
+const generateFileName = (
+  baseName: string,
+  extension: string = "xlsx"
+): string => {
+  const timestamp = dayjs().format("YYYYMMDD_HHmmss");
+  return `${baseName}_${timestamp}.${extension}`;
+};
+
+// 关闭对话框
+const handleClose = () => {
+  internalVisible.value = false;
+  resetForm();
+};
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+  }
+  form.startPage = "1";
+  form.endPage = "10";
+  loading.value = false;
+};
+
+const calculateNum = (param: any) => {
+  if (!param.pageSize) {
+    param.pageSize = 10;
+    return true;
+  }
+  if (param.startPage < 0) {
+    ElMessage.warning("行数必须大于0");
+    return false;
+  }
+  if (Number(param.startPage) > Number(param.endPage)) {
+    ElMessage.warning("结束行不能小于开始行");
+    return false;
+  }
+
+  if ((param.endPage - param.startPage) * param.pageSize > 500000) {
+    ElMessage.warning("导出行数不能超过5万");
+    return false;
+  }
+  return true;
+};
+
+// 处理导出
+const handleExport = async () => {
+  if (!formRef.value) return;
+
+  try {
+    // 验证表单
+    await formRef.value.validate();
+
+    loading.value = true;
+
+    let params = { ...props.queryParams };
+
+    if (!props.likeTable) {
+      params.endPage = form.endPage;
+      params.startPage = form.startPage;
+      if (!calculateNum(params)) {
+        return;
+      }
+    } else {
+      let rows =
+        (form.endPage - form.startPage + 1) * props.queryParams.pageSize;
+      params.pageNum = form.startPage;
+      params.pageSize = Math.abs(rows);
+    }
+
+    if (params.reportDate) {
+      // 将param.queryTime转为字符串,隔开
+      params.reportDate = params.reportDate.join();
+    }
+
+    if (params.multipleDate) {
+      params.multipleDate = params.multipleDate.join();
+    }
+
+    // 调用导出接口
+    const blob = await http.postExport<Blob, any>(props.url, { data: params });
+
+    // 生成文件名
+    const fullFileName = generateFileName(props.fileName);
+
+    // 下载文件
+    downloadFile(blob, fullFileName);
+
+    // 显示成功消息
+    ElMessage.success(`导出成功!文件名:${fullFileName}`);
+
+    // 关闭对话框
+    handleClose();
+  } catch (error) {
+    console.error("导出失败:", error);
+
+    // 根据错误类型显示不同的错误信息
+    let errorMessage = "导出失败,请稍后重试";
+
+    if (error?.response) {
+      // HTTP错误
+      const status = error.response.status;
+      switch (status) {
+        case 400:
+          errorMessage = "请求参数错误";
+          break;
+        case 401:
+          errorMessage = "未授权,请重新登录";
+          break;
+        case 403:
+          errorMessage = "没有导出权限";
+          break;
+        case 404:
+          errorMessage = "导出接口不存在";
+          break;
+        case 500:
+          errorMessage = "服务器内部错误";
+          break;
+        default:
+          errorMessage = `导出失败 (${status})`;
+      }
+    } else if (error?.code === "NETWORK_ERROR") {
+      errorMessage = "网络连接失败,请检查网络";
+    } else if (error?.message) {
+      errorMessage = error.message;
+    }
+
+    ElMessage.error(errorMessage);
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped>
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+:deep(.el-form-item__label) {
+  font-weight: 500;
+}
+
+:deep(.el-input__inner) {
+  text-align: center;
+}
+</style>

+ 19 - 1
src/utils/http/index.ts

@@ -7,7 +7,8 @@ import type {
   PureHttpError,
   RequestMethods,
   PureHttpResponse,
-  PureHttpRequestConfig
+  PureHttpRequestConfig,
+  FileHttpRequestConfig
 } from "./types.d";
 import { stringify } from "qs";
 import NProgress from "../progress";
@@ -189,6 +190,23 @@ class PureHttp {
   ): Promise<T> {
     return this.request<T>("get", url, params, config);
   }
+
+  /* ----------------------------------- 导出封装 ----------------------------------- */
+
+  public postExport<T, P>(
+    url: string,
+    params?: AxiosRequestConfig<P>,
+    config?: FileHttpRequestConfig
+  ): Promise<T> {
+    const exportConfig = {
+      responseType: "blob",
+      ...params,
+      ...config
+    } as PureHttpRequestConfig;
+
+    return this.request<T>("post", url, exportConfig);
+  }
+  /* ------------------------------------------------------------------------------- */
 }
 
 export const http = new PureHttp();

+ 9 - 0
src/utils/http/types.d.ts

@@ -27,6 +27,15 @@ export interface PureHttpRequestConfig extends AxiosRequestConfig {
   beforeResponseCallback?: (response: PureHttpResponse) => void;
 }
 
+export interface FileHttpResponse extends AxiosResponse {
+  config: FileHttpRequestConfig;
+}
+
+export interface FileHttpRequestConfig extends AxiosRequestConfig {
+  beforeRequestCallback?: (request: FileHttpRequestConfig) => void;
+  beforeResponseCallback?: (response: FileHttpResponse) => void;
+}
+
 export default class PureHttp {
   request<T>(
     method: RequestMethods,

+ 12 - 1
src/views/testTable/pageTable/index.vue

@@ -13,7 +13,16 @@
       }"
       :defaultPageInfo="{ page: 1, pageSize: 10 }"
       :defaultPageSizeList="[10, 20, 50, 100]"
-    />
+    >
+      <template #toolbar>
+        <ExportView
+          title="导出"
+          url="''"
+          file-name="订单列表"
+          :query-params="{}"
+        />
+      </template>
+    </PlusPage>
   </div>
 </template>
 
@@ -28,6 +37,8 @@ import {
 import { cloneDeep } from "lodash-es";
 import { number } from "echarts";
 
+import ExportView from "@/components/ExportView/index.vue";
+
 defineOptions({
   name: "PageTable"
 });

+ 0 - 8
src/views/testTable/splitTable/index.vue

@@ -278,9 +278,6 @@ const handleSrotChange = (values: any) => {
 const handleClickButton = (data: ButtonsCallBackParams) => {
   console.log("handleClickButton", data);
 };
-const handleExport = () => {
-  console.log("handleExport");
-};
 </script>
 
 <template>
@@ -318,11 +315,6 @@ const handleExport = () => {
         @sort-change="handleSrotChange"
         @clickAction="handleClickButton"
       >
-        <template #toolbar>
-          <el-button plain size="small" @click="handleExport"
-            >导出数据</el-button
-          >
-        </template>
       </PlusTable>
     </el-card>
   </div>