|
@@ -3,7 +3,7 @@
|
|
|
<div class="report_module score_pdf">
|
|
<div class="report_module score_pdf">
|
|
|
<template v-for="(item, page) in state.tableData">
|
|
<template v-for="(item, page) in state.tableData">
|
|
|
<div class="area_page print_page">
|
|
<div class="area_page print_page">
|
|
|
- <div class="area_title" v-if="item.isFirstPage">
|
|
|
|
|
|
|
+ <div class="area_title">
|
|
|
<div class="page_num">
|
|
<div class="page_num">
|
|
|
第{{ page + 1 }}页(共{{ state.pageTotal }}页)
|
|
第{{ page + 1 }}页(共{{ state.pageTotal }}页)
|
|
|
</div>
|
|
</div>
|
|
@@ -12,7 +12,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="print_time">打印时间:{{ getNowTime() }}</div>
|
|
<div class="print_time">打印时间:{{ getNowTime() }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="area_count" v-if="item.isFirstPage">
|
|
|
|
|
|
|
+ <div class="area_count">
|
|
|
应考:{{ item?.fiveRateResult?.examAllNum || 0 }}人
|
|
应考:{{ item?.fiveRateResult?.examAllNum || 0 }}人
|
|
|
实考:{{ item?.fiveRateResult?.effectiveNum || 0 }}人
|
|
实考:{{ item?.fiveRateResult?.effectiveNum || 0 }}人
|
|
|
缺考:{{
|
|
缺考:{{
|
|
@@ -70,13 +70,37 @@
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="page_dialog">
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ title="下载PDF"
|
|
|
|
|
+ v-model="showReportLoading"
|
|
|
|
|
+ width="500px"
|
|
|
|
|
+ top="15%"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="report_loading">
|
|
|
|
|
+ <div class="report_loading_icon">
|
|
|
|
|
+ <img src="@/assets/icon/report_loading.png" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="report_loading_title">正在努力生成PDF中,请稍等...</div>
|
|
|
|
|
+ <div class="report_loading_progress">
|
|
|
|
|
+ <el-progress
|
|
|
|
|
+ :text-inside="true"
|
|
|
|
|
+ color="#2E64FA"
|
|
|
|
|
+ text-color="#ffffff"
|
|
|
|
|
+ :stroke-width="16"
|
|
|
|
|
+ :percentage="targetProgress"
|
|
|
|
|
+ ></el-progress>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
import { studentTranscriptTitle, queryStudentTranscript } from "@/api/analysis";
|
|
import { studentTranscriptTitle, queryStudentTranscript } from "@/api/analysis";
|
|
|
import { useAnalysisStore } from "@/store/analysis";
|
|
import { useAnalysisStore } from "@/store/analysis";
|
|
|
import { getNowTime } from "@/utils/common";
|
|
import { getNowTime } from "@/utils/common";
|
|
|
-import { reactive, computed, nextTick } from "vue";
|
|
|
|
|
|
|
+import { reactive, computed,onUnmounted, nextTick, ref } from "vue";
|
|
|
import { ElMessage } from "element-plus";
|
|
import { ElMessage } from "element-plus";
|
|
|
import { jsPDF } from "jspdf";
|
|
import { jsPDF } from "jspdf";
|
|
|
import domtoimage from "dom-to-image";
|
|
import domtoimage from "dom-to-image";
|
|
@@ -111,6 +135,9 @@ const analysisStore = useAnalysisStore();
|
|
|
const examName = computed(() => {
|
|
const examName = computed(() => {
|
|
|
return analysisStore.analysisExamInfo.examName || "";
|
|
return analysisStore.analysisExamInfo.examName || "";
|
|
|
});
|
|
});
|
|
|
|
|
+const showReportLoading = ref(false);
|
|
|
|
|
+const targetProgress = ref(0);
|
|
|
|
|
+const progressInterval = ref(null);
|
|
|
const state = reactive<State>({
|
|
const state = reactive<State>({
|
|
|
keyWord: "",
|
|
keyWord: "",
|
|
|
selectClassList: [],
|
|
selectClassList: [],
|
|
@@ -176,14 +203,14 @@ const GetTableData = async () => {
|
|
|
state.tableLoading = false;
|
|
state.tableLoading = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
-//分页:超过一页自动换页,后续页不包含标题行和应考/实考/缺考统计行
|
|
|
|
|
|
|
+//分页:超过一页自动换页,标题行+统计行每页都显示,表头只在第一页显示
|
|
|
const tablePageData = (list: any[]) => {
|
|
const tablePageData = (list: any[]) => {
|
|
|
const trLineHeight = 21; //表格表头、tr行高
|
|
const trLineHeight = 21; //表格表头、tr行高
|
|
|
const printPageHeight = 1255; //打印页面高度 减掉padding 30px
|
|
const printPageHeight = 1255; //打印页面高度 减掉padding 30px
|
|
|
|
|
|
|
|
const rowNum = Math.floor(printPageHeight / trLineHeight); //每页可容纳行数 向下取整
|
|
const rowNum = Math.floor(printPageHeight / trLineHeight); //每页可容纳行数 向下取整
|
|
|
const firstPageRows = rowNum - 3; //第一页数据行数:标题行+统计行+表头行占3行
|
|
const firstPageRows = rowNum - 3; //第一页数据行数:标题行+统计行+表头行占3行
|
|
|
- const otherPageRows = rowNum; //后续页数据行数:无标题、无统计、无表头,数据行放满 原来是 rowNum - 1,后续页没有表头占位了,数据行放满
|
|
|
|
|
|
|
+ const otherPageRows = rowNum - 2; //后续页数据行数:标题行+统计行占2行,无表头
|
|
|
|
|
|
|
|
const pageList: any[] = [];
|
|
const pageList: any[] = [];
|
|
|
list.forEach((item) => {
|
|
list.forEach((item) => {
|
|
@@ -224,6 +251,7 @@ const PageInit = async (
|
|
|
state.selectClassList = selectClassList;
|
|
state.selectClassList = selectClassList;
|
|
|
state.sortType = sortType;
|
|
state.sortType = sortType;
|
|
|
state.filterType = filterType;
|
|
state.filterType = filterType;
|
|
|
|
|
+ targetProgress.value = 0;
|
|
|
await Promise.all([GetStudentTranscriptTitle(), GetTableData()]);
|
|
await Promise.all([GetStudentTranscriptTitle(), GetTableData()]);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -355,8 +383,12 @@ const ExportPdf = async () => {
|
|
|
);
|
|
);
|
|
|
if (!elements.length) {
|
|
if (!elements.length) {
|
|
|
ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
|
|
ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
|
|
|
|
|
+ showReportLoading.value = false;
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ const pageCount = elements.length;
|
|
|
|
|
+ let oneProgress = Math.round((1 / pageCount) * 100);
|
|
|
|
|
+ StartProgress(0, oneProgress);
|
|
|
// 1. 确保页面 DOM 渲染完成(只在最开始等一次,不再逐页等待)
|
|
// 1. 确保页面 DOM 渲染完成(只在最开始等一次,不再逐页等待)
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
|
|
|
|
@@ -397,17 +429,22 @@ const ExportPdf = async () => {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
// 6. 并发截图:多页并行,限制并发数防止内存峰值过高
|
|
// 6. 并发截图:多页并行,限制并发数防止内存峰值过高
|
|
|
- // 机器内存大、页面多时可以调大到 6-8;页面很大时可降到 2
|
|
|
|
|
- const CONCURRENCY = 4;
|
|
|
|
|
|
|
+ // 999 条数据页数较多,并发 6 加快截图;页面很大时可降到 2-4
|
|
|
|
|
+ const CONCURRENCY = 6;
|
|
|
const dataUrls: string[] = new Array(elements.length);
|
|
const dataUrls: string[] = new Array(elements.length);
|
|
|
let cursor = 0;
|
|
let cursor = 0;
|
|
|
const worker = async () => {
|
|
const worker = async () => {
|
|
|
while (cursor < elements.length) {
|
|
while (cursor < elements.length) {
|
|
|
const pageIndex = cursor++;
|
|
const pageIndex = cursor++;
|
|
|
|
|
+ // 更新进度
|
|
|
|
|
+ const progress = Math.floor((pageIndex / pageCount) * 100);
|
|
|
|
|
+ const lastProgressprogress = progress - oneProgress;
|
|
|
|
|
+ StartProgress(lastProgressprogress, progress);
|
|
|
const htmlElement = elements[pageIndex];
|
|
const htmlElement = elements[pageIndex];
|
|
|
- // 渲染参数与原来完全一致,保留 scale(2)
|
|
|
|
|
|
|
+ // 渲染参数与原来完全一致,保留 scale(2);quality 0.6 在表格类内容上肉眼
|
|
|
|
|
+ // 几乎无差,但单页 JPEG 体积明显变小,PDF 整体更小、打开/翻页更快
|
|
|
dataUrls[pageIndex] = await domtoimage.toJpeg(htmlElement, {
|
|
dataUrls[pageIndex] = await domtoimage.toJpeg(htmlElement, {
|
|
|
- quality: 0.7,
|
|
|
|
|
|
|
+ quality: 0.6,
|
|
|
width: htmlElement.scrollWidth * 2,
|
|
width: htmlElement.scrollWidth * 2,
|
|
|
height: htmlElement.scrollHeight * 2,
|
|
height: htmlElement.scrollHeight * 2,
|
|
|
canvasWidth: htmlElement.scrollWidth * 2,
|
|
canvasWidth: htmlElement.scrollWidth * 2,
|
|
@@ -426,9 +463,8 @@ const ExportPdf = async () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
await Promise.all(
|
|
await Promise.all(
|
|
|
- Array.from(
|
|
|
|
|
- { length: Math.min(CONCURRENCY, elements.length) },
|
|
|
|
|
- () => worker(),
|
|
|
|
|
|
|
+ Array.from({ length: Math.min(CONCURRENCY, elements.length) }, () =>
|
|
|
|
|
+ worker(),
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
|
|
|
|
@@ -459,12 +495,13 @@ const ExportPdf = async () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// PDF 全部生成完后再开新标签,直接打开 PDF(不提前开窗、不写中转页、不用 iframe)
|
|
// PDF 全部生成完后再开新标签,直接打开 PDF(不提前开窗、不写中转页、不用 iframe)
|
|
|
- const filename = `${examName.value}_${analysisStore?.filterObject?.subjectName || ''}_${analysisStore?.filterObject?.classGroupName || ''}成绩单.pdf`;
|
|
|
|
|
|
|
+ const filename = `${examName.value}_${analysisStore?.filterObject?.subjectName || ""}_${analysisStore?.filterObject?.classGroupName || ""}成绩单.pdf`;
|
|
|
pdf.setProperties({ title: filename.replace(/\.pdf$/, "") });
|
|
pdf.setProperties({ title: filename.replace(/\.pdf$/, "") });
|
|
|
const blob = pdf.output("blob");
|
|
const blob = pdf.output("blob");
|
|
|
const url = URL.createObjectURL(
|
|
const url = URL.createObjectURL(
|
|
|
new File([blob], filename, { type: "application/pdf" }),
|
|
new File([blob], filename, { type: "application/pdf" }),
|
|
|
);
|
|
);
|
|
|
|
|
+ showReportLoading.value = false;
|
|
|
// 此时已不在用户手势里,可能被弹窗拦截;被拦就退化为直接下载
|
|
// 此时已不在用户手势里,可能被弹窗拦截;被拦就退化为直接下载
|
|
|
const win = window.open(url, "_blank");
|
|
const win = window.open(url, "_blank");
|
|
|
if (!win) {
|
|
if (!win) {
|
|
@@ -480,6 +517,54 @@ const ExportPdf = async () => {
|
|
|
window.focus();
|
|
window.focus();
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
+// 开始进度条数值变化 最大增加进度值 maxIncrement
|
|
|
|
|
+const StartProgress = (currentValue, targetValue) => {
|
|
|
|
|
+ // 清除已存在的定时器
|
|
|
|
|
+ if (progressInterval.value) {
|
|
|
|
|
+ clearInterval(progressInterval.value);
|
|
|
|
|
+ progressInterval.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前进度值
|
|
|
|
|
+ if (targetProgress.value < currentValue) {
|
|
|
|
|
+ targetProgress.value = currentValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 注意:这里不能再叫 targetProgress,否则会把外面的 ref 影子掉
|
|
|
|
|
+ const targetVal = targetValue;
|
|
|
|
|
+ // 如果当前值大于等于目标值,则不需要动画(进度不能递减)
|
|
|
|
|
+ if (targetProgress.value >= targetVal) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算差值
|
|
|
|
|
+ const difference = targetVal - currentValue;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据差值计算时间间隔:差值1->500ms,差值10->50ms
|
|
|
|
|
+ const minInterval = 50;
|
|
|
|
|
+ const maxInterval = 500;
|
|
|
|
|
+ const clampedDifference = Math.max(1, Math.min(difference, 10));
|
|
|
|
|
+ let interval =
|
|
|
|
|
+ maxInterval - (clampedDifference - 1) * ((maxInterval - minInterval) / 9);
|
|
|
|
|
+ interval = Math.round(interval / 10) * 10;
|
|
|
|
|
+ interval = Math.max(minInterval, Math.min(interval, maxInterval));
|
|
|
|
|
+ // 计算每次增加的步长(差值越大,步长越大)
|
|
|
|
|
+ const step = Math.max(1, Math.floor(difference / 5)) || 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建新的定时器
|
|
|
|
|
+ progressInterval.value = setInterval(() => {
|
|
|
|
|
+ // 只能增加,不能减少
|
|
|
|
|
+ targetProgress.value = Math.min(
|
|
|
|
|
+ targetProgress.value + step,
|
|
|
|
|
+ targetVal,
|
|
|
|
|
+ );
|
|
|
|
|
+ // 如果达到目标值,清除定时器
|
|
|
|
|
+ if (targetProgress.value >= targetVal) {
|
|
|
|
|
+ targetProgress.value = targetVal;
|
|
|
|
|
+ clearInterval(progressInterval.value);
|
|
|
|
|
+ progressInterval.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, interval);
|
|
|
|
|
+};
|
|
|
// 加载数据并导出PDF(供父组件一步调用)
|
|
// 加载数据并导出PDF(供父组件一步调用)
|
|
|
const LoadAndExportPdf = async (
|
|
const LoadAndExportPdf = async (
|
|
|
keyWord: string,
|
|
keyWord: string,
|
|
@@ -487,11 +572,20 @@ const LoadAndExportPdf = async (
|
|
|
sortType: number,
|
|
sortType: number,
|
|
|
filterType: number,
|
|
filterType: number,
|
|
|
) => {
|
|
) => {
|
|
|
|
|
+ // 点按钮立刻弹框,不要等数据加载完再弹
|
|
|
|
|
+ showReportLoading.value = true;
|
|
|
|
|
+ targetProgress.value = 0;
|
|
|
|
|
+ await nextTick();
|
|
|
await PageInit(keyWord, selectClassList, sortType, filterType);
|
|
await PageInit(keyWord, selectClassList, sortType, filterType);
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
await ExportPdf();
|
|
await ExportPdf();
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ if (progressInterval.value) {
|
|
|
|
|
+ clearInterval(progressInterval.value);
|
|
|
|
|
+ progressInterval.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
// 暴露方法供父组件调用
|
|
// 暴露方法供父组件调用
|
|
|
defineExpose({
|
|
defineExpose({
|
|
|
PageInit,
|
|
PageInit,
|