|
@@ -0,0 +1,452 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="report_module error">
|
|
|
|
|
+ <div class="area_page">
|
|
|
|
|
+ <div class="area_title">
|
|
|
|
|
+ <div class="page_num">第1页(共10页)</div>
|
|
|
|
|
+ <div class="title_text">
|
|
|
|
|
+ 【{{ `${examName}错题名单(${className})` }}】
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="print_time">打印时间:{{ getNowTime() }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="area_page print_page">
|
|
|
|
|
+ <div class="area_title">
|
|
|
|
|
+ <div class="page_num">第1页(共10页)</div>
|
|
|
|
|
+ <div class="title_text">
|
|
|
|
|
+ 【{{ `${examName}错题名单(${className})` }}】
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="print_time">打印时间:{{ getNowTime() }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="module_table table_42">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ :data="tablePageData"
|
|
|
|
|
+ border
|
|
|
|
|
+ stripe
|
|
|
|
|
+ row-key="questionId"
|
|
|
|
|
+ :span-method="SpanMethod"
|
|
|
|
|
+ :row-class-name="ErrorTableRowClassName"
|
|
|
|
|
+ :cell-class-name="ErrorTableCellClassName"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="questionName"
|
|
|
|
|
+ label="小题名称"
|
|
|
|
|
+ width="60"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="questionType"
|
|
|
|
|
+ label="题型"
|
|
|
|
|
+ width="50"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="questionScore"
|
|
|
|
|
+ label="满分"
|
|
|
|
|
+ width="48"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="averageScore"
|
|
|
|
|
+ label="平均分"
|
|
|
|
|
+ width="48"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="scoreRate"
|
|
|
|
|
+ label="得分率"
|
|
|
|
|
+ width="50"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="studentCount"
|
|
|
|
|
+ label="答题人数"
|
|
|
|
|
+ width="55"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="errorCount"
|
|
|
|
|
+ label="错题人数"
|
|
|
|
|
+ width="55"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="answerValue"
|
|
|
|
|
+ label="正确答案"
|
|
|
|
|
+ width="55"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ {{ scope.row.answerValue ?? "-" }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="errorDetail" label="错题详情" align="center">
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ :align="key == 'registrationCodeList' ? 'left' : 'center'"
|
|
|
|
|
+ v-for="(key, index) in [
|
|
|
|
|
+ 'name',
|
|
|
|
|
+ 'rate',
|
|
|
|
|
+ 'studentNum',
|
|
|
|
|
+ 'registrationCodeList',
|
|
|
|
|
+ ]"
|
|
|
|
|
+ :key="key"
|
|
|
|
|
+ label=""
|
|
|
|
|
+ :prop="key"
|
|
|
|
|
+ :min-width="[28, 50, 30, 320][index]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="{ row: org }">
|
|
|
|
|
+ <span v-if="key == 'registrationCodeList'">{{ org[key] }}</span>
|
|
|
|
|
+ <span v-else-if="key == 'rate'">{{
|
|
|
|
|
+ org[key] == "-" ? "-" : `${org[key]}%`
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ <span v-else>{{ org[key] }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+import { getNowTime } from "@/utils/common";
|
|
|
|
|
+import { ref, nextTick, watch } from "vue";
|
|
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
|
|
+import { jsPDF } from "jspdf";
|
|
|
|
|
+import domtoimage from "dom-to-image";
|
|
|
|
|
+// 定义数据类型
|
|
|
|
|
+interface TableItem {
|
|
|
|
|
+ rowKey: number | string;
|
|
|
|
|
+ questionName?: string;
|
|
|
|
|
+ questionType?: string;
|
|
|
|
|
+ answerValue?: string;
|
|
|
|
|
+ questionScore?: number | string;
|
|
|
|
|
+ averageScore?: number | string;
|
|
|
|
|
+ scoreRate?: number | string;
|
|
|
|
|
+ hiddenRatioStu?: boolean;
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+ rate?: number | string;
|
|
|
|
|
+ studentNum?: number | string;
|
|
|
|
|
+ studentCount?: number | string;
|
|
|
|
|
+ registrationCodeList?: string;
|
|
|
|
|
+ hiddenRatio?: boolean;
|
|
|
|
|
+ hiddenStu?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 定义 Props
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ examName: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "",
|
|
|
|
|
+ },
|
|
|
|
|
+ tableData: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
|
|
+ subjectName: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "",
|
|
|
|
|
+ },
|
|
|
|
|
+ className: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "",
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+const tablePageData = ref<TableItem[][]>([]);
|
|
|
|
|
+// 定义 Emits
|
|
|
|
|
+const emit = defineEmits(["PdfLoadEnd"]);
|
|
|
|
|
+
|
|
|
|
|
+// Refs & 常量
|
|
|
|
|
+const pageTitle = ref<HTMLElement | null>(null);
|
|
|
|
|
+const titleHeight = ref(80);
|
|
|
|
|
+const questionHeight = 30;
|
|
|
|
|
+const lineHeight = 16;
|
|
|
|
|
+const printPageHeight = 1205;
|
|
|
|
|
+
|
|
|
|
|
+// 表格分页逻辑
|
|
|
|
|
+const TablePagesData = () => {};
|
|
|
|
|
+
|
|
|
|
|
+// 下载 PDF (使用 dom-to-image)
|
|
|
|
|
+const DownloadPdf = async () => {
|
|
|
|
|
+ const elements = document.querySelectorAll(".page_report .print_page");
|
|
|
|
|
+ if (elements.length === 0) {
|
|
|
|
|
+ ElMessage.warning("未找到可导出的页面元素!");
|
|
|
|
|
+ emit("PdfLoadEnd");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pdf = new jsPDF("p", "pt", "a4");
|
|
|
|
|
+ const pdfWidth = pdf.internal.pageSize.getWidth();
|
|
|
|
|
+ const pdfHeight = pdf.internal.pageSize.getHeight();
|
|
|
|
|
+
|
|
|
|
|
+ for (const [pageIndex, element] of Array.from(elements).entries()) {
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ if (element) {
|
|
|
|
|
+ const htmlElement = element as HTMLElement;
|
|
|
|
|
+ htmlElement.style.visibility = "visible";
|
|
|
|
|
+ htmlElement.offsetHeight;
|
|
|
|
|
+
|
|
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
|
+
|
|
|
|
|
+ const dataUrl = await domtoimage.toJpeg(htmlElement, {
|
|
|
|
|
+ quality: 0.8,
|
|
|
|
|
+ width: htmlElement.scrollWidth * 2,
|
|
|
|
|
+ height: htmlElement.scrollHeight * 2,
|
|
|
|
|
+ canvasWidth: htmlElement.scrollWidth * 2,
|
|
|
|
|
+ canvasHeight: htmlElement.scrollHeight * 2,
|
|
|
|
|
+ style: {
|
|
|
|
|
+ transform: "scale(2)",
|
|
|
|
|
+ "transform-origin": "top left",
|
|
|
|
|
+ "background-color": "#ffffff",
|
|
|
|
|
+ overflow: "visible",
|
|
|
|
|
+ "white-space": "normal",
|
|
|
|
|
+ position: "relative",
|
|
|
|
|
+ visibility: "visible",
|
|
|
|
|
+ },
|
|
|
|
|
+ bgcolor: "#ffffff",
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const imgProps = pdf.getImageProperties(dataUrl);
|
|
|
|
|
+ const imgWidth = imgProps.width;
|
|
|
|
|
+ const imgHeight = imgProps.height;
|
|
|
|
|
+
|
|
|
|
|
+ const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight);
|
|
|
|
|
+ const adjustWidth = imgWidth * ratio;
|
|
|
|
|
+ const adjustHeight = imgHeight * ratio;
|
|
|
|
|
+
|
|
|
|
|
+ const xPosition = (pdfWidth - adjustWidth) / 2;
|
|
|
|
|
+ const yPosition = (pdfHeight - adjustHeight) / 2;
|
|
|
|
|
+
|
|
|
|
|
+ if (pageIndex > 0) {
|
|
|
|
|
+ pdf.addPage();
|
|
|
|
|
+ }
|
|
|
|
|
+ pdf.addImage(
|
|
|
|
|
+ dataUrl,
|
|
|
|
|
+ "JPEG",
|
|
|
|
|
+ xPosition,
|
|
|
|
|
+ yPosition,
|
|
|
|
|
+ adjustWidth,
|
|
|
|
|
+ adjustHeight,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pdf.save(
|
|
|
|
|
+ `${props.examName}_${props.subjectName}_${props.className}错题名单.pdf`,
|
|
|
|
|
+ );
|
|
|
|
|
+ emit("PdfLoadEnd");
|
|
|
|
|
+};
|
|
|
|
|
+const LoadData = () => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const pageTitleHeight = pageTitle.value?.offsetHeight || 60;
|
|
|
|
|
+ titleHeight.value = pageTitleHeight + 20;
|
|
|
|
|
+ tablePageData.value = props.tableData;
|
|
|
|
|
+ TablePagesData();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+// 错题分析 合并单元格
|
|
|
|
|
+const SpanMethod = ({ row, column }: any) => {
|
|
|
|
|
+ const props = [
|
|
|
|
|
+ "errorDetail",
|
|
|
|
|
+ "name",
|
|
|
|
|
+ "rate",
|
|
|
|
|
+ "studentNum",
|
|
|
|
|
+ "registrationCodeList",
|
|
|
|
|
+ ];
|
|
|
|
|
+ if (props.indexOf(column.property) == -1) {
|
|
|
|
|
+ if (row.rowspan) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ rowspan: row.rowspan,
|
|
|
|
|
+ colspan: row.colspan,
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return {
|
|
|
|
|
+ rowspan: 0,
|
|
|
|
|
+ colspan: 0,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 定义行样式
|
|
|
|
|
+const ErrorTableRowClassName = ({ row }: any) => {
|
|
|
|
|
+ if (row.rowspanIndex % 2 === 0) {
|
|
|
|
|
+ return "row_color_FFFFFF";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return "row_color_FAFAFA";
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 设置单元格样式
|
|
|
|
|
+const ErrorTableCellClassName = ({ column }: any) => {
|
|
|
|
|
+ if (column.property == "registrationCodeList") {
|
|
|
|
|
+ return "white_space_normal";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+// 监听筛选条件
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.tableData,
|
|
|
|
|
+ async () => {
|
|
|
|
|
+ if (props.tableData.length) {
|
|
|
|
|
+ LoadData();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true },
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// 暴露方法供父组件调用
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ DownloadPdf,
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.error {
|
|
|
|
|
+ width: 908px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ // position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: -9999px;
|
|
|
|
|
+ z-index: -10;
|
|
|
|
|
+
|
|
|
|
|
+ .area_page {
|
|
|
|
|
+ width: 908px;
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ padding: 10px !important;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &.print_page {
|
|
|
|
|
+ height: 1285px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .area_title {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ line-height: 30px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+
|
|
|
|
|
+ .page_num {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #000000;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .print_time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #000000;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .title_text {
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #000000;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .module_table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ border-collapse: collapse;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-table) {
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border-left: 0px;
|
|
|
|
|
+ border-right: 0px;
|
|
|
|
|
+ thead {
|
|
|
|
|
+ height: 23px !important;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .el-table__header {
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ td {
|
|
|
|
|
+ &.is-left {
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 23px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ .el-table__header-wrapper {
|
|
|
|
|
+ .el-table__header {
|
|
|
|
|
+ .is-group {
|
|
|
|
|
+ tr {
|
|
|
|
|
+ &:nth-child(2) {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-table__cell {
|
|
|
|
|
+ height: 23px !important;
|
|
|
|
|
+ &.white_space_normal {
|
|
|
|
|
+ .cell {
|
|
|
|
|
+ white-space: normal;
|
|
|
|
|
+ padding: 0 0 0 3px !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.el-table--striped {
|
|
|
|
|
+ .el-table__body {
|
|
|
|
|
+ tr {
|
|
|
|
|
+ height: 23px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ &.el-table__row--striped {
|
|
|
|
|
+ &.row_color_FFFFFF {
|
|
|
|
|
+ td.el-table__cell {
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.el-table--striped
|
|
|
|
|
+ .el-table__body
|
|
|
|
|
+ tr.el-table__row.row_color_FAFAFA
|
|
|
|
|
+ td.el-table__cell {
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.el-table--striped
|
|
|
|
|
+ .el-table__body
|
|
|
|
|
+ tr.el-table__row--striped.row_color_FAFAFA
|
|
|
|
|
+ td.el-table__cell {
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.el-table--enable-row-hover .el-table__body tr.row_color_FFFFFF > td {
|
|
|
|
|
+ background-color: transparent !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.table_row_blue) {
|
|
|
|
|
+ color: #2e64fa;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|