index.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <template>
  2. <el-button plain size="small" @click="internalVisible = true">
  3. 导出数据
  4. </el-button>
  5. <el-dialog
  6. v-model="internalVisible"
  7. center
  8. :close-on-click-modal="false"
  9. destroy-on-close
  10. :title="dialogTitle"
  11. width="480px"
  12. :before-close="handleClose"
  13. >
  14. <el-form ref="formRef" :model="form" :rules="rules" @submit.prevent>
  15. <el-row justify="center">
  16. <el-form-item prop="startPage">
  17. <el-input
  18. v-model="form.startPage"
  19. placeholder="请输入开始页码"
  20. clearable
  21. style="max-width: 120px"
  22. @keyup.enter="handleExport"
  23. />
  24. </el-form-item>
  25. <span style="margin: 5px 10px 0">至</span>
  26. <el-form-item prop="endPage">
  27. <el-input
  28. v-model="form.endPage"
  29. placeholder="请输入结束页码"
  30. clearable
  31. style="max-width: 120px"
  32. @keyup.enter="handleExport"
  33. />
  34. </el-form-item>
  35. </el-row>
  36. </el-form>
  37. <template #footer>
  38. <div class="dialog-footer">
  39. <el-button @click="handleClose">取消</el-button>
  40. <el-button type="primary" :loading="loading" @click="handleExport">
  41. 导出
  42. </el-button>
  43. </div>
  44. </template>
  45. </el-dialog>
  46. </template>
  47. <script setup lang="ts">
  48. import { ref, reactive, computed } from "vue";
  49. import type { FormInstance, FormRules } from "element-plus";
  50. import { ElMessage } from "element-plus";
  51. import dayjs from "dayjs";
  52. import { http } from "@/utils/http";
  53. // 定义接口
  54. interface ExportForm {
  55. startPage: any;
  56. endPage: any;
  57. }
  58. // Props 定义
  59. const props = withDefaults(
  60. defineProps<{
  61. title?: string;
  62. url: string;
  63. fileName: string;
  64. queryParams?: any;
  65. likeTable?: boolean;
  66. }>(),
  67. {
  68. title: "导出数据",
  69. likeTable: false
  70. }
  71. );
  72. // 响应式数据
  73. const internalVisible = ref(false);
  74. const loading = ref(false);
  75. const formRef = ref<FormInstance>();
  76. // 计算属性
  77. const dialogTitle = computed(() => props.title);
  78. // 表单数据
  79. const form = reactive<ExportForm>({
  80. startPage: "1",
  81. endPage: "10"
  82. });
  83. /**
  84. * @description 从1开始正整数校验
  85. * @param val
  86. * @returns {boolean}
  87. */
  88. const isNumbAvailable = (val: any): boolean => {
  89. let nubReg = /^[1-9]\d*$/;
  90. return nubReg.test(val);
  91. };
  92. // 自定义验证函数
  93. const validatePageNumber = (rule: any, value: string, callback: any) => {
  94. if (!value) {
  95. callback(new Error("请输入页码"));
  96. return;
  97. }
  98. if (!isNumbAvailable(value)) {
  99. callback(new Error("请输入有效的正整数"));
  100. return;
  101. }
  102. callback();
  103. };
  104. const validateEndPage = (rule: any, value: string, callback: any) => {
  105. if (!value) {
  106. callback(new Error("请输入结束页码"));
  107. return;
  108. }
  109. if (!isNumbAvailable(value)) {
  110. callback(new Error("请输入有效的正整数"));
  111. return;
  112. }
  113. const startPageNum = parseInt(form.startPage);
  114. const endPageNum = parseInt(value);
  115. if (endPageNum < startPageNum) {
  116. callback(new Error("结束页不能小于开始页"));
  117. return;
  118. }
  119. callback();
  120. };
  121. // 表单验证规则
  122. const rules = reactive<FormRules<ExportForm>>({
  123. startPage: [{ validator: validatePageNumber, trigger: "blur" }],
  124. endPage: [{ validator: validateEndPage, trigger: "blur" }]
  125. });
  126. /**
  127. * 下载文件
  128. * @param blob 文件blob对象
  129. * @param fileName 文件名
  130. */
  131. const downloadFile = (blob: Blob, fileName: string) => {
  132. try {
  133. const url = window.URL.createObjectURL(blob);
  134. const link = document.createElement("a");
  135. link.href = url;
  136. link.download = fileName;
  137. link.style.display = "none";
  138. document.body.appendChild(link);
  139. link.click();
  140. document.body.removeChild(link);
  141. window.URL.revokeObjectURL(url);
  142. } catch (error) {
  143. console.error("文件下载失败:", error);
  144. ElMessage.error("文件下载失败");
  145. }
  146. };
  147. /**
  148. * 生成文件名
  149. * @param baseName 基础文件名
  150. * @param extension 文件扩展名
  151. * @returns 完整文件名
  152. */
  153. const generateFileName = (
  154. baseName: string,
  155. extension: string = "xlsx"
  156. ): string => {
  157. const timestamp = dayjs().format("YYYYMMDD_HHmmss");
  158. return `${baseName}_${timestamp}.${extension}`;
  159. };
  160. // 关闭对话框
  161. const handleClose = () => {
  162. internalVisible.value = false;
  163. resetForm();
  164. };
  165. // 重置表单
  166. const resetForm = () => {
  167. if (formRef.value) {
  168. formRef.value.resetFields();
  169. }
  170. form.startPage = "1";
  171. form.endPage = "10";
  172. loading.value = false;
  173. };
  174. const calculateNum = (param: any) => {
  175. if (!param.pageSize) {
  176. param.pageSize = 10;
  177. return true;
  178. }
  179. if (param.startPage < 0) {
  180. ElMessage.warning("行数必须大于0");
  181. return false;
  182. }
  183. if (Number(param.startPage) > Number(param.endPage)) {
  184. ElMessage.warning("结束行不能小于开始行");
  185. return false;
  186. }
  187. if ((param.endPage - param.startPage) * param.pageSize > 500000) {
  188. ElMessage.warning("导出行数不能超过5万");
  189. return false;
  190. }
  191. return true;
  192. };
  193. // 处理导出
  194. const handleExport = async () => {
  195. if (!formRef.value) return;
  196. try {
  197. // 验证表单
  198. await formRef.value.validate();
  199. loading.value = true;
  200. let params = { ...props.queryParams };
  201. if (!props.likeTable) {
  202. params.endPage = form.endPage;
  203. params.startPage = form.startPage;
  204. if (!calculateNum(params)) {
  205. return;
  206. }
  207. } else {
  208. let rows =
  209. (form.endPage - form.startPage + 1) * props.queryParams.pageSize;
  210. params.pageNum = form.startPage;
  211. params.pageSize = Math.abs(rows);
  212. }
  213. if (params.reportDate) {
  214. // 将param.queryTime转为字符串,隔开
  215. params.reportDate = params.reportDate.join();
  216. }
  217. if (params.multipleDate) {
  218. params.multipleDate = params.multipleDate.join();
  219. }
  220. // 调用导出接口
  221. const blob = await http.postExport<Blob, any>(props.url, { data: params });
  222. // 生成文件名
  223. const fullFileName = generateFileName(props.fileName);
  224. // 下载文件
  225. downloadFile(blob, fullFileName);
  226. // 显示成功消息
  227. ElMessage.success(`导出成功!文件名:${fullFileName}`);
  228. // 关闭对话框
  229. handleClose();
  230. } catch (error) {
  231. console.error("导出失败:", error);
  232. // 根据错误类型显示不同的错误信息
  233. let errorMessage = "导出失败,请稍后重试";
  234. if (error?.response) {
  235. // HTTP错误
  236. const status = error.response.status;
  237. switch (status) {
  238. case 400:
  239. errorMessage = "请求参数错误";
  240. break;
  241. case 401:
  242. errorMessage = "未授权,请重新登录";
  243. break;
  244. case 403:
  245. errorMessage = "没有导出权限";
  246. break;
  247. case 404:
  248. errorMessage = "导出接口不存在";
  249. break;
  250. case 500:
  251. errorMessage = "服务器内部错误";
  252. break;
  253. default:
  254. errorMessage = `导出失败 (${status})`;
  255. }
  256. } else if (error?.code === "NETWORK_ERROR") {
  257. errorMessage = "网络连接失败,请检查网络";
  258. } else if (error?.message) {
  259. errorMessage = error.message;
  260. }
  261. ElMessage.error(errorMessage);
  262. } finally {
  263. loading.value = false;
  264. }
  265. };
  266. </script>
  267. <style scoped>
  268. .dialog-footer {
  269. display: flex;
  270. justify-content: flex-end;
  271. gap: 12px;
  272. }
  273. :deep(.el-form-item__label) {
  274. font-weight: 500;
  275. }
  276. :deep(.el-input__inner) {
  277. text-align: center;
  278. }
  279. </style>