dengshaobo пре 2 месеци
родитељ
комит
f46e99bf81
100 измењених фајлова са 14521 додато и 902 уклоњено
  1. 515 134
      package-lock.json
  2. 5 0
      package.json
  3. 43 19
      public/index.html
  4. 0 0
      public/math.js
  5. 0 0
      public/output/chtml.js
  6. 0 0
      public/output/chtml/fonts/tex.js
  7. BIN
      public/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff
  8. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff
  9. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff
  10. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff
  11. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff
  12. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff
  13. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff
  14. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff
  15. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff
  16. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff
  17. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff
  18. BIN
      public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff
  19. BIN
      public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff
  20. BIN
      public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff
  21. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff
  22. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff
  23. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff
  24. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff
  25. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff
  26. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff
  27. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff
  28. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff
  29. BIN
      public/output/chtml/fonts/woff-v2/MathJax_Zero.woff
  30. 0 0
      public/output/svg.js
  31. 0 0
      public/output/svg/fonts/tex.js
  32. 12 8
      src/App.vue
  33. BIN
      src/assets/bg/student_report_cover.webp
  34. BIN
      src/assets/icon/close.webp
  35. BIN
      src/assets/report/header_left_student.webp
  36. BIN
      src/assets/report/header_right_student.webp
  37. BIN
      src/assets/report/score_dimidiate_icon.webp
  38. BIN
      src/assets/report/score_no_icon.webp
  39. BIN
      src/assets/report/score_yes_icon.webp
  40. BIN
      src/assets/report/turn_next.webp
  41. BIN
      src/assets/report/turn_prev.webp
  42. 1 0
      src/assets/studentAnalysis/analysis.svg
  43. BIN
      src/assets/studentAnalysis/basket.png
  44. 1 0
      src/assets/studentAnalysis/card.svg
  45. 1 0
      src/assets/studentAnalysis/circleBlue.svg
  46. 1 0
      src/assets/studentAnalysis/circleGrey.svg
  47. 1 0
      src/assets/studentAnalysis/collection.svg
  48. 1 0
      src/assets/studentAnalysis/echarts.svg
  49. 1 0
      src/assets/studentAnalysis/iconBlue.svg
  50. 1 0
      src/assets/studentAnalysis/iconGrey.svg
  51. 2 0
      src/components/FiltersItem_ruoyan.vue
  52. 351 137
      src/components/PaperImage.vue
  53. 10 4
      src/components/StudentPaper.vue
  54. 4 1
      src/http/api.js
  55. 1 1
      src/http/api/errorQuestion.js
  56. 35 0
      src/http/api/personalProfile.js
  57. 11 3
      src/http/api/reportStudent.js
  58. 5 1
      src/main.js
  59. 1 1
      src/plugins/lib-flexible/flexible.js
  60. 55 39
      src/router/index.js
  61. 6 1
      src/store/modules/question.js
  62. 1045 92
      src/styles/report.scss
  63. 159 93
      src/utils/common.js
  64. 1 1
      src/utils/scan.js
  65. 3352 0
      src/utils/turn.js
  66. 408 0
      src/views/analysisReport/components/benchTaskSelectSchool.vue
  67. 1 1
      src/views/analysisReport/components/dCharts/GaugeChart.vue
  68. 130 80
      src/views/analysisReport/components/dCharts/barChart.vue
  69. 36 12
      src/views/analysisReport/components/dCharts/differenceChart.vue
  70. 46 22
      src/views/analysisReport/components/dCharts/lineChart.vue
  71. 66 14
      src/views/analysisReport/components/dCharts/radarCharts.vue
  72. 141 0
      src/views/analysisReport/personalProfile/Download.vue
  73. 520 0
      src/views/analysisReport/personalProfile/HistoricalChangeChart.vue
  74. 133 0
      src/views/analysisReport/personalProfile/KnowledgeTrack.vue
  75. 359 0
      src/views/analysisReport/personalProfile/MyGradeHistory.vue
  76. 743 0
      src/views/analysisReport/personalProfile/index.vue
  77. 260 0
      src/views/analysisReport/personalProfile/knowledgePaps.vue
  78. 59 0
      src/views/analysisReport/personalProfile/tools.js
  79. 1914 0
      src/views/analysisReport/personalProfile/zeroScoreKnowledge.vue
  80. 124 0
      src/views/analysisReport/studentPage/downloadPdf/components/bookCover.vue
  81. 759 0
      src/views/analysisReport/studentPage/downloadPdf/components/bookFlip.vue
  82. 262 0
      src/views/analysisReport/studentPage/downloadPdf/components/css/magazine.css
  83. BIN
      src/views/analysisReport/studentPage/downloadPdf/components/image/loader.gif
  84. 325 0
      src/views/analysisReport/studentPage/downloadPdf/components/js/magazine.js
  85. 26 0
      src/views/analysisReport/studentPage/downloadPdf/components/js/zoom.min.js
  86. 1665 0
      src/views/analysisReport/studentPage/downloadPdf/studentReport.vue
  87. 7 4
      src/views/analysisReport/studentPage/list/mainPage.vue
  88. 95 20
      src/views/analysisReport/studentPage/mainPage.vue
  89. 19 4
      src/views/analysisReport/studentPage/scrolReport/transcript.vue
  90. 192 33
      src/views/analysisReport/studentPage/scrolReport/transcript_single.vue
  91. 61 23
      src/views/analysisReport/studentPage/scrolReport/transcript_total.vue
  92. 0 0
      src/views/analysisReport/wrongQuestion/AllQuestion.vue
  93. 0 0
      src/views/analysisReport/wrongQuestion/HasQuestion.vue
  94. 1 1
      src/views/analysisReport/wrongQuestion/KnowledgePoint.vue
  95. 0 0
      src/views/analysisReport/wrongQuestion/NoQuestion.vue
  96. 11 0
      src/views/analysisReport/wrongQuestion/Notbook.vue
  97. 256 0
      src/views/analysisReport/wrongQuestion/Preview.vue
  98. 193 66
      src/views/analysisReport/wrongQuestion/index.vue
  99. 87 86
      src/views/analysisReport/wrongQuestion/loadImg.js
  100. 1 1
      src/views/layout/components/AppMain.vue

Разлика између датотеке није приказан због своје велике величине
+ 515 - 134
package-lock.json


+ 5 - 0
package.json

@@ -20,13 +20,17 @@
     "core-js": "^3.38.1",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
+    "dom-to-image": "^2.6.0",
     "echarts": "^5.5.1",
     "element-ui": "^2.15.14",
     "font-awesome": "^4.7.0",
     "html-webpack-plugin": "^5.6.0",
+    "html2canvas": "^1.4.1",
+    "jquery": "^3.5.1",
     "js-cookie": "^3.0.5",
     "js-md5": "^0.8.3",
     "jsencrypt": "^3.3.2",
+    "jspdf": "^2.5.2",
     "kde": "^0.0.1",
     "lottie-web": "^5.12.2",
     "nprogress": "^0.2.0",
@@ -48,6 +52,7 @@
     "vue-virtual-scroller": "^1.1.2",
     "vuex": "^3.6.2",
     "vuex-persistedstate": "^4.1.0",
+    "vxe-table": "^3.20.0",
     "xe-utils": "^3.5.13",
     "yarn": "^1.22.22"
   },

+ 43 - 19
public/index.html

@@ -1,21 +1,45 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-    <meta name="referrer" content="no-referrer" />
-    <script defer src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title><%= htmlWebpackPlugin.options.title %></title>
-   
-    <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4939100_wnefpnxejys.css">
-    
-  </head>
-  <body>
-    <noscript>
-      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
-    </noscript>
-    <div id="app"></div>
-  </body>
-</html>                         
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <meta name="referrer" content="no-referrer" />
+  <script defer src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
+  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+  <title>
+    <%= htmlWebpackPlugin.options.title %>
+  </title>
+
+  <script>
+    window.MathJax = {
+      loader: {
+        // 指定 mathjax 资源根路径为当前 public 目录(如需放到子目录请修改)
+        paths: { mathjax: './' },
+        // 只加载必要的输入/输出模块,避免自动加载辅助无障碍模块(会触发 speech-worker)
+        load: ['input/tex', 'output/chtml']
+      },
+      chtml: {
+        // 指向本地字体目录(建议把 MathJax 的 woff-v2 字体复制到 public/output/chtml/fonts/woff-v2)
+        fontURL: './output/chtml/fonts/woff-v2'
+      },
+      tex: {
+        inlineMath: [['\\(', '\\)'], ['$', '$']]
+      }
+    };
+  </script>
+  <script src="./math.js"></script>
+
+  <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4939100_wnefpnxejys.css">
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+        Please enable it to continue.</strong>
+  </noscript>
+  <div id="app"></div>
+</body>
+
+</html>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/math.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/output/chtml.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/output/chtml/fonts/tex.js


BIN
public/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff


BIN
public/output/chtml/fonts/woff-v2/MathJax_Zero.woff


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/output/svg.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/output/svg/fonts/tex.js


+ 12 - 8
src/App.vue

@@ -1,27 +1,31 @@
 <template>
     <div id="app" ref="app">
-        <router-view></router-view>
+        <router-view :key="$route.fullPath"></router-view>
     </div>
 </template>
 
 <script>
 import user from "@/http/api/user";
-import { setToken } from "@/utils/auth";
+import { setToken, getToken } from "@/utils/auth";
 import { encrypt } from "@/utils/jsencrypt";
 export default {
     name: "App",
     mounted() {
         // INFO: 本地调试,自动登录
         if (process.env.NODE_ENV === 'development') {
-            // this.SubmitLogin();
+            this.SubmitLogin();
         }
     },
     methods: {
         SubmitLogin() {
-            const username = '50002100001';
-            const password = '50002100001';
-            const type = '2';
-            sessionStorage.setItem('schoolType', type);
+            const username = '376050cy_xiyanzuo_015';
+            const password = '123456';
+            // const type = '1';
+            const schoolType = sessionStorage.getItem('schoolType');
+            if (getToken() && schoolType) {
+                return
+            }
+            sessionStorage.setItem('schoolType', schoolType || 2);
             user.loginEmailPass({
                 username: username,
                 password: encrypt(password),
@@ -33,7 +37,7 @@ export default {
                     this.$store.dispatch("user/SET_SCHOOL_LOGO", res.data.schoolLogoUrl);//设置学校logo
                     this.$store.dispatch("user/SET_SCHOOL_WEB_SITE_ID", res.data.cloudMonitorSiteId || '');//设置学校网站id
                     this.$store.dispatch("user/getUserInfoByToken");//获取用户登录信息
-                    this.$router.push("/studentAnalysisReport/list");
+                    this.$router.push("/jointStudentAnalysisReport/list");
                 }
             });
         },

BIN
src/assets/bg/student_report_cover.webp


BIN
src/assets/icon/close.webp


BIN
src/assets/report/header_left_student.webp


BIN
src/assets/report/header_right_student.webp


BIN
src/assets/report/score_dimidiate_icon.webp


BIN
src/assets/report/score_no_icon.webp


BIN
src/assets/report/score_yes_icon.webp


BIN
src/assets/report/turn_next.webp


BIN
src/assets/report/turn_prev.webp


+ 1 - 0
src/assets/studentAnalysis/analysis.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770262130832" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1870" width="20" height="20" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M874.624 68.288a102.4 102.4 0 0 1 102.4 102.4v682.624a102.4 102.4 0 0 1-102.336 102.4H256a38.4 38.4 0 0 1 0-76.8h618.688a25.6 25.6 0 0 0 25.6-25.6V401.088H123.648V512a38.4 38.4 0 1 1-76.8 0V170.688a102.4 102.4 0 0 1 102.4-102.4h725.312z m-234.496 453.184a38.4 38.4 0 0 1 46.08 5.76l95.616 93.12a38.4 38.4 0 0 1-53.568 55.04l-70.272-68.48-144.832 126.656a38.4 38.4 0 0 1-51.84-1.216L348.16 624.064l-233.088 235.648a38.4 38.4 0 0 1-54.656-54.08l259.776-262.4a38.4 38.4 0 0 1 53.888-0.704l114.944 110.08 145.152-126.848 5.952-4.288zM149.312 145.088c-14.08 0-25.6 11.52-25.6 25.6v153.6h776.512v-153.6a25.6 25.6 0 0 0-25.6-25.6H149.312z m130.432 51.2a38.4 38.4 0 0 1 0 76.8h-42.688a38.4 38.4 0 0 1 0-76.8h42.688z m512 0a38.4 38.4 0 0 1 0 76.8h-384a38.4 38.4 0 0 1 0-76.8h384z" p-id="1871" fill="#303133"></path></svg>

BIN
src/assets/studentAnalysis/basket.png


+ 1 - 0
src/assets/studentAnalysis/card.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770083294111" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2518" width="14" height="14" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M927.656 219.877H96.004c-17.673 0-32 14.327-32 32v520c0 17.673 14.327 32 32 32h831.652c17.673 0 32-14.327 32-32v-520c0-17.672-14.327-32-32-32z m-32 520H128.004v-456h767.652v456z" fill="#333333" p-id="2519"></path><path d="M254.241 633.451c0.053-0.146 5.452-14.838 18.675-28.833 16.321-17.274 37.47-26.033 62.86-26.033 25.637 0 47.345 8.914 64.52 26.494 13.891 14.219 19.853 29.107 19.912 29.256l-0.098-0.258 59.956-22.389c-0.958-2.566-9.986-25.738-31.587-48.82-14.717-15.726-31.768-27.758-50.632-35.88 21.708-17.923 35.567-45.031 35.567-75.315 0-53.838-43.8-97.638-97.638-97.638s-97.638 43.8-97.638 97.638c0 30.334 13.905 57.48 35.676 75.402-18.733 8.194-35.523 20.342-49.85 36.224-21.018 23.298-29.346 46.698-30.226 49.29l60.599 20.586-0.096 0.276z m81.535-212.139c16.742 0 30.362 13.621 30.362 30.362s-13.621 30.362-30.362 30.362-30.362-13.621-30.362-30.362 13.62-30.362 30.362-30.362zM557.112 448h243.257c17.673 0 32-14.327 32-32s-14.327-32-32-32H557.112c-17.673 0-32 14.327-32 32s14.327 32 32 32zM557.112 576.838h146.257c17.673 0 32-14.327 32-32s-14.327-32-32-32H557.112c-17.673 0-32 14.327-32 32s14.327 32 32 32z" fill="#333333" p-id="2520"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/circleBlue.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770198059469" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2149" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M516.8 102.4a76.8 76.8 0 0 1 0 153.6l2.88-0.192v56.832a38.4 38.4 0 0 1 32.96 32.832l0.32 5.248a38.4 38.4 0 0 1-38.4 38.4l5.12-0.384v72.576a51.2 51.2 0 0 1 23.68 10.176l52.8-52.928a38.4 38.4 0 0 1 53.376-53.44l44.544-44.544a76.8 76.8 0 1 1 9.6 12.096l-43.52 43.52a38.4 38.4 0 0 1-52.864 53.12l-53.312 53.312a51.648 51.648 0 0 1 8.896 23.104h62.784a38.4 38.4 0 0 1 74.752 0.128l83.84 0.064a76.8 76.8 0 1 1-1.6 15.36h-81.728a38.4 38.4 0 0 1-75.712-0.128l-62.784-0.064a51.008 51.008 0 0 1-10.048 22.4l42.56 42.88a38.4 38.4 0 0 1 49.728 58.24l66.752 59.264a76.8 76.8 0 1 1-6.656 6.016l-4.352 4.736-63.488-64a38.4 38.4 0 0 1-42.24-2.24l-4.352-3.776a38.4 38.4 0 0 1-5.76-46.72l-43.392-43.84a50.88 50.88 0 0 1-21.504 8.576v62.336a38.4 38.4 0 0 1-5.12 76.48l5.12-0.384v86.72a76.8 76.8 0 0 1 73.6 69.312l0.32 7.424a76.8 76.8 0 1 1-89.28-75.776v-88.704a38.4 38.4 0 0 1 0-74.048v-63.36a50.816 50.816 0 0 1-15.04-4.736l-42.24 42.432a38.4 38.4 0 0 1-51.52 51.584l-62.528 62.592a76.8 76.8 0 1 1-11.072-10.752l61.44-61.44a38.4 38.4 0 0 1 56.512-51.968l-2.56-2.112 39.296-39.232a51.072 51.072 0 0 1-14.912-27.52H383.36a38.4 38.4 0 0 1-75.648-0.192l-56.96-0.064 0.064 0.64A76.8 76.8 0 0 1 181.504 598.4l-7.424 0.384a76.8 76.8 0 1 1 75.136-92.864h59.008a38.4 38.4 0 0 1 74.88 0.128h78.08a50.88 50.88 0 0 1 8.832-23.296l-52.864-53.248a38.4 38.4 0 0 1-52.736-53.248l-38.912-39.232a76.8 76.8 0 1 1 10.432-11.328l39.232 39.424a38.4 38.4 0 0 1 53.248 53.696l52.288 52.8a50.944 50.944 0 0 1 23.616-10.176V387.712a38.4 38.4 0 0 1 0-74.048v-58.688A76.8 76.8 0 0 1 516.8 102.4z" p-id="2150" fill="#2E64FA"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/circleGrey.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770198059469" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2149" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M516.8 102.4a76.8 76.8 0 0 1 0 153.6l2.88-0.192v56.832a38.4 38.4 0 0 1 32.96 32.832l0.32 5.248a38.4 38.4 0 0 1-38.4 38.4l5.12-0.384v72.576a51.2 51.2 0 0 1 23.68 10.176l52.8-52.928a38.4 38.4 0 0 1 53.376-53.44l44.544-44.544a76.8 76.8 0 1 1 9.6 12.096l-43.52 43.52a38.4 38.4 0 0 1-52.864 53.12l-53.312 53.312a51.648 51.648 0 0 1 8.896 23.104h62.784a38.4 38.4 0 0 1 74.752 0.128l83.84 0.064a76.8 76.8 0 1 1-1.6 15.36h-81.728a38.4 38.4 0 0 1-75.712-0.128l-62.784-0.064a51.008 51.008 0 0 1-10.048 22.4l42.56 42.88a38.4 38.4 0 0 1 49.728 58.24l66.752 59.264a76.8 76.8 0 1 1-6.656 6.016l-4.352 4.736-63.488-64a38.4 38.4 0 0 1-42.24-2.24l-4.352-3.776a38.4 38.4 0 0 1-5.76-46.72l-43.392-43.84a50.88 50.88 0 0 1-21.504 8.576v62.336a38.4 38.4 0 0 1-5.12 76.48l5.12-0.384v86.72a76.8 76.8 0 0 1 73.6 69.312l0.32 7.424a76.8 76.8 0 1 1-89.28-75.776v-88.704a38.4 38.4 0 0 1 0-74.048v-63.36a50.816 50.816 0 0 1-15.04-4.736l-42.24 42.432a38.4 38.4 0 0 1-51.52 51.584l-62.528 62.592a76.8 76.8 0 1 1-11.072-10.752l61.44-61.44a38.4 38.4 0 0 1 56.512-51.968l-2.56-2.112 39.296-39.232a51.072 51.072 0 0 1-14.912-27.52H383.36a38.4 38.4 0 0 1-75.648-0.192l-56.96-0.064 0.064 0.64A76.8 76.8 0 0 1 181.504 598.4l-7.424 0.384a76.8 76.8 0 1 1 75.136-92.864h59.008a38.4 38.4 0 0 1 74.88 0.128h78.08a50.88 50.88 0 0 1 8.832-23.296l-52.864-53.248a38.4 38.4 0 0 1-52.736-53.248l-38.912-39.232a76.8 76.8 0 1 1 10.432-11.328l39.232 39.424a38.4 38.4 0 0 1 53.248 53.696l52.288 52.8a50.944 50.944 0 0 1 23.616-10.176V387.712a38.4 38.4 0 0 1 0-74.048v-58.688A76.8 76.8 0 0 1 516.8 102.4z" p-id="2150" fill="#999999"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/collection.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770106604115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14431" data-spm-anchor-id="a313x.search_index.0.i13.5d733a81rLbCs3" width="20" height="20" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M781.186088 616.031873q17.338645 80.573705 30.59761 145.848606 6.119522 27.537849 11.219124 55.075697t9.689243 49.976096 7.649402 38.247012 4.079681 19.888446q3.059761 20.398406-9.179283 27.027888t-27.537849 6.629482q-5.099602 0-14.788845-3.569721t-14.788845-5.609562l-266.199203-155.027888q-72.414343 42.836653-131.569721 76.494024-25.498008 14.278884-50.486056 28.557769t-45.386454 26.517928-35.187251 20.398406-19.888446 10.199203q-10.199203 5.099602-20.908367 3.569721t-19.378486-7.649402-12.749004-14.788845-2.039841-17.848606q1.01992-4.079681 5.099602-19.888446t9.179283-37.737052 11.729084-48.446215 13.768924-54.055777q15.298805-63.23506 34.677291-142.788845-60.175299-52.015936-108.111554-92.812749-20.398406-17.338645-40.286853-34.167331t-35.697211-30.59761-26.007968-22.438247-11.219124-9.689243q-12.239044-11.219124-20.908367-24.988048t-6.629482-28.047809 11.219124-22.438247 20.398406-10.199203l315.155378-28.557769 117.290837-273.338645q6.119522-16.318725 17.338645-28.047809t30.59761-11.729084q10.199203 0 17.848606 4.589641t12.749004 10.709163 8.669323 12.239044 5.609562 10.199203l114.231076 273.338645 315.155378 29.577689q20.398406 5.099602 28.557769 12.239044t8.159363 22.438247q0 14.278884-8.669323 24.988048t-21.928287 26.007968z" p-id="14432" fill="#999999"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/echarts.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769670608772" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6499" width="14" height="14" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M494.5 926c-53.5 0-105.4-10.5-154.3-31.2-47.2-20-89.6-48.6-126-85-36.4-36.4-65-78.8-85-126C108.5 634.9 98 583 98 529.5c0-53.4 10.5-105.3 31.1-154.1 19.9-47.2 48.4-89.5 84.7-125.9 36.3-36.4 78.6-65 125.7-85 48.8-20.7 100.6-31.3 154-31.4l25.1-0.1v372.4H891l-0.1 25.1c-0.1 53.4-10.7 105.2-31.4 154-20 47.1-48.6 89.4-85 125.7s-78.8 64.8-125.9 84.7C599.8 915.5 547.9 926 494.5 926z m-25.9-742C289.7 197.3 148 347.3 148 529.5 148 720.6 303.4 876 494.5 876c182.2 0 332.2-141.7 345.5-320.6H468.6V184z" p-id="6500" fill="#666666"></path><path d="M583 98v343h343c0-189.4-153.6-343-343-343z" p-id="6501" fill="#666666"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/iconBlue.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770193137434" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8464" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M419.037 287.953h413.124c17.673 0 32-14.327 32-32s-14.327-32-32-32H419.037c-17.673 0-32 14.327-32 32s14.327 32 32 32zM419.028 543.17h411.608c17.673 0 32-14.327 32-32s-14.327-32-32-32H419.028c-17.673 0-32 14.327-32 32s14.327 32 32 32zM832.161 735.802H419.037c-17.673 0-32 14.327-32 32s14.327 32 32 32h413.124c17.673 0 32-14.327 32-32s-14.327-32-32-32z" fill="#2E64FA" p-id="8465"></path><path d="M256.037 255.953m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#2E64FA" p-id="8466"></path><path d="M256.037 510.787m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#2E64FA" p-id="8467"></path><path d="M256.037 767.621m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#2E64FA" p-id="8468"></path></svg>

+ 1 - 0
src/assets/studentAnalysis/iconGrey.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770193137434" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8464" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M419.037 287.953h413.124c17.673 0 32-14.327 32-32s-14.327-32-32-32H419.037c-17.673 0-32 14.327-32 32s14.327 32 32 32zM419.028 543.17h411.608c17.673 0 32-14.327 32-32s-14.327-32-32-32H419.028c-17.673 0-32 14.327-32 32s14.327 32 32 32zM832.161 735.802H419.037c-17.673 0-32 14.327-32 32s14.327 32 32 32h413.124c17.673 0 32-14.327 32-32s-14.327-32-32-32z" fill="#999999" p-id="8465"></path><path d="M256.037 255.953m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#999999" p-id="8466"></path><path d="M256.037 510.787m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#999999" p-id="8467"></path><path d="M256.037 767.621m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#999999" p-id="8468"></path></svg>

+ 2 - 0
src/components/FiltersItem_ruoyan.vue

@@ -24,6 +24,8 @@
       return {
       }
     },
+    mounted() {
+    },
     methods:{
 
       //筛选点击事件

+ 351 - 137
src/components/PaperImage.vue

@@ -1,8 +1,8 @@
 <template>
 
   <div class="paper_container"  ref="paperContainer"  @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"  @wheel="onWheel">
-      <canvas id="paperCanvas" ref="paperCanvas" class="paper_canvas" @mouseleave="onCanvasLeave"></canvas>
-      <div :id="`imgContainer${imageIndex}`" class="img_container">
+      <canvas id="paperCanvas" ref="paperCanvas" class="paper_canvas" @mouseleave="onCanvasLeave" :style="{transform: `rotate(${rotateDeg}deg)`}"></canvas>
+      <div ref="imgContainer" :id="`imgContainer${imageIndex}`" class="img_container" :style="{transform: `rotate(${rotateDeg}deg)`}">
         <img v-if="paperImgUrl" :src="paperImgUrl" >
       </div>
       <div class="no_paper_url" v-if="paperImgUrl==''">
@@ -50,6 +50,20 @@ export default {
       type:Boolean,
       default:true
     },//是否可以拖动 默认可以拖动
+    isWheel:{
+      type:Boolean,
+      default:true
+    },//是否可以滚动放大、缩小 默认可以滚动
+
+    isShowContextMenu:{
+      type:Boolean,
+      default:true
+    },//是否显示右键菜单
+
+    rotateDeg:{
+      type:String,
+      default:'0'
+    },//答题卡方向
 
     isAbnormal:{
       type:Boolean,
@@ -263,6 +277,7 @@ export default {
       // 处理右键点击事件
       handleRightClick(event) 
       {
+        if(!this.isShowContextMenu) return false
         event.preventDefault();
         event.stopPropagation(); // 阻止事件继续向上冒泡
         // 设置菜单位置(相对于 paper_container 的位置)
@@ -655,146 +670,257 @@ export default {
               const drawLineData=JSON.parse(item.drawLineData);
               console.log("打印drawLineData",drawLineData);
 
-
-              // for(var j=0;j<drawLineData.length;j++)
-              // {
-              //   var drawItem=drawLineData[j];
-
-              //   console.log("打印drawItem",drawItem);
-              //   // 扣分点 加分点 标记// 1:文字 (扣分留痕 显示扣分信息)  2:划线  3:波浪线   4:画笔 5:评语
-              //   if(drawItem.drawType==1)
-              //   {
-                  
-              //     const fontSize = Math.max(12, 40 * this.zoomRate* this.scale); // 最小字体12px,基础字体16px
-              //     ctx.font = `${fontSize}px Arial`; // 让文字大小跟随缩放
-              //     // ctx.font = '30px Arial';
-              //     ctx.fillStyle = '#D81E06';
-              //     ctx.textAlign = 'center';
-              //     ctx.textBaseline = 'middle';
-              //     if(drawItem.type=='reduce')
-              //     {
-              //       ctx.fillText('-'+drawItem.score,(drawItem.x + blockPoint.x) * this.zoomRate * this.scale, (drawItem.y + blockPoint.y) * this.zoomRate * this.scale);  
-              //     }
-              //     if(drawItem.type=='bonus')
-              //     {
-              //       ctx.fillText('+'+drawItem.score, (drawItem.x + blockPoint.x) * this.zoomRate * this.scale, (drawItem.y + blockPoint.y) * this.zoomRate * this.scale);  
-              //     }
+              if(blockList.length>0)
+              {
+                //跨页的
+                blockList.forEach((blockItem,index) => {
+                  console.log("打印blockItem",blockItem);
+                  if(blockItem.page==this.currentPage)
+                  {
+                    drawLineData.forEach((drawlineItem,drawIndex) => {
+                      let findIndex=this.FindBlockIndex(drawlineItem,blockList);
+                      console.log("打印drawIndex",drawIndex);
+                      console.log("打印findIndex",findIndex);
+                      
+                      let drawItem=drawlineItem;
+                      if(findIndex==index)
+                      {
+                        drawItem=this.FindTargetObj(drawlineItem,blockList);
+                        blockPoint=blockList[findIndex];
+      
+        
+                        console.log("打印blockPoint",blockPoint);
+                        console.log("打印drawItem",drawItem);
+                        // 计算通用的缩放因子,避免重复计算
+                        const zoomScale = zoomRate * scale;
+                        const offsetX = blockPoint.x * zoomScale;
+                        const offsetY = blockPoint.y * zoomScale;
+                        // 扣分点 加分点 标记// 1:文字 (扣分留痕 显示扣分信息)  2:划线  3:波浪线   4:画笔 5:评语
+                        switch(drawItem.drawType) {
+                          case 1: {
+                            // 文字类型(扣分点/加分点标记)
+                            const fontSize = Math.max(12, 40 * zoomScale);
+                            ctx.font = `${fontSize}px Arial`;
+                            ctx.fillStyle = '#D81E06';
+                            ctx.textAlign = 'center';
+                            ctx.textBaseline = 'middle';
+                            
+                            const x = drawItem.x * zoomScale + offsetX;
+                            const y = drawItem.y * zoomScale + offsetY;
+                            
+                            if(drawItem.type==='reduce') {
+                              ctx.fillText('-'+drawItem.score, x, y);  
+                            } else if(drawItem.type==='bonus') {
+                              ctx.fillText('+'+drawItem.score, x, y);  
+                            }
+                            break;
+                          }
+                          
+                          case 2: {
+                            // 绘制横线
+                            console.log("打印需要减去的高度",jHeight);
+                            jHeight=0;
+                            console.log("打印blockList",blockList);
+                            const coords = {
+                              x: this.GetInteger(drawItem.x * zoomScale + offsetX),
+                              y: this.GetInteger(drawItem.y * zoomScale + offsetY - jHeight * zoomScale),
+                              endX: this.GetInteger(drawItem.endX * zoomScale + offsetX),
+                              endY: this.GetInteger(drawItem.endY * zoomScale + offsetY - jHeight * zoomScale)
+                            };
+                            this.DrawHorizontalLine(coords.x, coords.y, coords.endX, coords.endY,ctx);
+                            break;
+                          }
+                          
+                          case 3: {
+                            // 绘制波浪线
+                            const coords = {
+                              startX: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+                              startY: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2)),
+                              endX: parseFloat((drawItem.endX * zoomScale + offsetX).toFixed(2)),
+                              endY: parseFloat((drawItem.endY * zoomScale + offsetY).toFixed(2))
+                            };
+                            this.DrawWaveLine(coords.startX, coords.startY, coords.endX, coords.endY,ctx);
+                            break;
+                          }
+                          
+                          case 4:
+                            // 绘制画笔数据
+                            this.DrawPenLine(drawItem,blockPoint,zoomRate,scale,ctx);
+                            break;
+                          
+                          case 5: {
+                            // 绘制评语
+                            const coords = {
+                              x: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+                              y: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2))
+                            };
+                            this.DrawText(coords.x, coords.y, drawItem.text,ctx);
+                            break;
+                          }
+                          
+                          default:
+                            console.warn('未知的绘图类型:', drawItem.drawType);
+                        }
+                      }
+                      
+                    })
+                  }
                   
-              //   }
-              //   else if(drawItem.drawType==2)
-              //   {
-              //     //绘制横线
-              //     console.log("打印需要减去的高度",jHeight);
-              //     jHeight=0;
-              //     console.log("打印blockList",blockList);
-              //     let drawX=this.GetInteger((drawItem.x+ blockPoint.x)* this.zoomRate * this.scale);
-              //     let drawY=this.GetInteger((drawItem.y+blockPoint.y - jHeight)* this.zoomRate * this.scale);
-              //     let drawEndX=this.GetInteger((drawItem.endX+ blockPoint.x)* this.zoomRate * this.scale);
-              //     let drawEndY=this.GetInteger((drawItem.endY+blockPoint.y - jHeight)* this.zoomRate * this.scale);
-              //     this.DrawHorizontalLine(drawX,drawY,drawEndX,drawEndY);
-              //     // this.DrawHorizontalLine((drawItem.x+ blockPoint.x)* this.zoomRate * this.scale, (drawItem.y+blockPoint.y)* this.zoomRate * this.scale,(drawItem.endX+ blockPoint.x)* this.zoomRate * this.scale, (drawItem.endY+blockPoint.y)* this.zoomRate * this.scale);
-
-              //   }
-              //   else if(drawItem.drawType==3)
-              //   {
-              //     //绘制波浪线
-              //     let startX=parseFloat(((drawItem.x + blockPoint.x)* this.zoomRate * this.scale).toFixed(2));
-              //     let startY=parseFloat(((drawItem.y + blockPoint.y)* this.zoomRate * this.scale).toFixed(2));
-              //     let endX=parseFloat(((drawItem.endX + blockPoint.x)* this.zoomRate * this.scale).toFixed(2));
-              //     let endY=parseFloat(((drawItem.endY +blockPoint.y)* this.zoomRate * this.scale).toFixed(2));
+                });
+              }
+              else
+              {
+                for(const [index, drawItem] of drawLineData.entries()) {
+                  console.log("打印drawItem",drawItem);
+                  // 计算通用的缩放因子,避免重复计算
+                  const zoomScale = zoomRate * scale;
+                  const offsetX = blockPoint.x * zoomScale;
+                  const offsetY = blockPoint.y * zoomScale;
                   
-              //     this.DrawWaveLine(startX, startY,endX, endY);
-              //   }
-              //   else if(drawItem.drawType==4)
-              //   {
-              //     //绘制画笔数据
-              //     // console.log("打印画笔数据",drawItem);
-              //     this.DrawPenLine(drawItem,blockPoint);
-              //   }
-              //   else if(drawItem.drawType==5)
-              //   {
-              //     //绘制评语
-              //     let startX=parseFloat(((drawItem.x + blockPoint.x)* this.zoomRate * this.scale).toFixed(2));
-              //     let startY=parseFloat(((drawItem.y + blockPoint.y)* this.zoomRate * this.scale).toFixed(2));
-              //     // console.log("打印绘制评语数据",drawItem);
-              //     this.DrawText(startX,startY,drawItem.text);
-              //   }
-              // }
-
-              for(const [index, drawItem] of drawLineData.entries()) {
-                console.log("打印drawItem",drawItem);
-                // 计算通用的缩放因子,避免重复计算
-                const zoomScale = zoomRate * scale;
-                const offsetX = blockPoint.x * zoomScale;
-                const offsetY = blockPoint.y * zoomScale;
-                
-                // 扣分点 加分点 标记// 1:文字 (扣分留痕 显示扣分信息)  2:划线  3:波浪线   4:画笔 5:评语
-                switch(drawItem.drawType) {
-                  case 1: {
-                    // 文字类型(扣分点/加分点标记)
-                    const fontSize = Math.max(12, 40 * zoomScale);
-                    ctx.font = `${fontSize}px Arial`;
-                    ctx.fillStyle = '#D81E06';
-                    ctx.textAlign = 'center';
-                    ctx.textBaseline = 'middle';
+                  // 扣分点 加分点 标记// 1:文字 (扣分留痕 显示扣分信息)  2:划线  3:波浪线   4:画笔 5:评语
+                  switch(drawItem.drawType) {
+                    case 1: {
+                      // 文字类型(扣分点/加分点标记)
+                      const fontSize = Math.max(12, 40 * zoomScale);
+                      ctx.font = `${fontSize}px Arial`;
+                      ctx.fillStyle = '#D81E06';
+                      ctx.textAlign = 'center';
+                      ctx.textBaseline = 'middle';
+                      
+                      const x = drawItem.x * zoomScale + offsetX;
+                      const y = drawItem.y * zoomScale + offsetY;
+                      
+                      if(drawItem.type==='reduce') {
+                        ctx.fillText('-'+drawItem.score, x, y);  
+                      } else if(drawItem.type==='bonus') {
+                        ctx.fillText('+'+drawItem.score, x, y);  
+                      }
+                      break;
+                    }
                     
-                    const x = drawItem.x * zoomScale + offsetX;
-                    const y = drawItem.y * zoomScale + offsetY;
+                    case 2: {
+                      // 绘制横线
+                      console.log("打印需要减去的高度",jHeight);
+                      jHeight=0;
+                      console.log("打印blockList",blockList);
+                      const coords = {
+                        x: this.GetInteger(drawItem.x * zoomScale + offsetX),
+                        y: this.GetInteger(drawItem.y * zoomScale + offsetY - jHeight * zoomScale),
+                        endX: this.GetInteger(drawItem.endX * zoomScale + offsetX),
+                        endY: this.GetInteger(drawItem.endY * zoomScale + offsetY - jHeight * zoomScale)
+                      };
+                      this.DrawHorizontalLine(coords.x, coords.y, coords.endX, coords.endY,ctx);
+                      break;
+                    }
                     
-                    if(drawItem.type==='reduce') {
-                      ctx.fillText('-'+drawItem.score, x, y);  
-                    } else if(drawItem.type==='bonus') {
-                      ctx.fillText('+'+drawItem.score, x, y);  
+                    case 3: {
+                      // 绘制波浪线
+                      const coords = {
+                        startX: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+                        startY: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2)),
+                        endX: parseFloat((drawItem.endX * zoomScale + offsetX).toFixed(2)),
+                        endY: parseFloat((drawItem.endY * zoomScale + offsetY).toFixed(2))
+                      };
+                      this.DrawWaveLine(coords.startX, coords.startY, coords.endX, coords.endY,ctx);
+                      break;
                     }
-                    break;
+                    
+                    case 4:
+                      // 绘制画笔数据
+                      this.DrawPenLine(drawItem,blockPoint,zoomRate,scale,ctx);
+                      break;
+                    
+                    case 5: {
+                      // 绘制评语
+                      const coords = {
+                        x: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+                        y: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2))
+                      };
+                      this.DrawText(coords.x, coords.y, drawItem.text,ctx);
+                      break;
+                    }
+                    
+                    default:
+                      console.warn('未知的绘图类型:', drawItem.drawType);
                   }
+                }
+              }
+             
+
+              // for(const [index, drawItem] of drawLineData.entries()) {
+              //   console.log("打印drawItem",drawItem);
+              //   // 计算通用的缩放因子,避免重复计算
+              //   const zoomScale = zoomRate * scale;
+              //   const offsetX = blockPoint.x * zoomScale;
+              //   const offsetY = blockPoint.y * zoomScale;
+                
+              //   // 扣分点 加分点 标记// 1:文字 (扣分留痕 显示扣分信息)  2:划线  3:波浪线   4:画笔 5:评语
+              //   switch(drawItem.drawType) {
+              //     case 1: {
+              //       // 文字类型(扣分点/加分点标记)
+              //       const fontSize = Math.max(12, 40 * zoomScale);
+              //       ctx.font = `${fontSize}px Arial`;
+              //       ctx.fillStyle = '#D81E06';
+              //       ctx.textAlign = 'center';
+              //       ctx.textBaseline = 'middle';
+                    
+              //       const x = drawItem.x * zoomScale + offsetX;
+              //       const y = drawItem.y * zoomScale + offsetY;
+                    
+              //       if(drawItem.type==='reduce') {
+              //         ctx.fillText('-'+drawItem.score, x, y);  
+              //       } else if(drawItem.type==='bonus') {
+              //         ctx.fillText('+'+drawItem.score, x, y);  
+              //       }
+              //       break;
+              //     }
                   
-                  case 2: {
-                    // 绘制横线
-                    console.log("打印需要减去的高度",jHeight);
-                    jHeight=0;
-                    console.log("打印blockList",blockList);
-                    const coords = {
-                      x: this.GetInteger(drawItem.x * zoomScale + offsetX),
-                      y: this.GetInteger(drawItem.y * zoomScale + offsetY - jHeight * zoomScale),
-                      endX: this.GetInteger(drawItem.endX * zoomScale + offsetX),
-                      endY: this.GetInteger(drawItem.endY * zoomScale + offsetY - jHeight * zoomScale)
-                    };
-                    this.DrawHorizontalLine(coords.x, coords.y, coords.endX, coords.endY);
-                    break;
-                  }
+              //     case 2: {
+              //       // 绘制横线
+              //       console.log("打印需要减去的高度",jHeight);
+              //       jHeight=0;
+              //       console.log("打印blockList",blockList);
+              //       const coords = {
+              //         x: this.GetInteger(drawItem.x * zoomScale + offsetX),
+              //         y: this.GetInteger(drawItem.y * zoomScale + offsetY - jHeight * zoomScale),
+              //         endX: this.GetInteger(drawItem.endX * zoomScale + offsetX),
+              //         endY: this.GetInteger(drawItem.endY * zoomScale + offsetY - jHeight * zoomScale)
+              //       };
+              //       this.DrawHorizontalLine(coords.x, coords.y, coords.endX, coords.endY);
+              //       break;
+              //     }
                   
-                  case 3: {
-                    // 绘制波浪线
-                    const coords = {
-                      startX: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
-                      startY: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2)),
-                      endX: parseFloat((drawItem.endX * zoomScale + offsetX).toFixed(2)),
-                      endY: parseFloat((drawItem.endY * zoomScale + offsetY).toFixed(2))
-                    };
-                    this.DrawWaveLine(coords.startX, coords.startY, coords.endX, coords.endY);
-                    break;
-                  }
+              //     case 3: {
+              //       // 绘制波浪线
+              //       const coords = {
+              //         startX: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+              //         startY: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2)),
+              //         endX: parseFloat((drawItem.endX * zoomScale + offsetX).toFixed(2)),
+              //         endY: parseFloat((drawItem.endY * zoomScale + offsetY).toFixed(2))
+              //       };
+              //       this.DrawWaveLine(coords.startX, coords.startY, coords.endX, coords.endY);
+              //       break;
+              //     }
                   
-                  case 4:
-                    // 绘制画笔数据
-                    this.DrawPenLine(drawItem,blockPoint);
-                    break;
+              //     case 4:
+              //       // 绘制画笔数据
+              //       this.DrawPenLine(drawItem,blockPoint);
+              //       break;
                   
-                  case 5: {
-                    // 绘制评语
-                    const coords = {
-                      x: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
-                      y: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2))
-                    };
-                    this.DrawText(coords.x, coords.y, drawItem.text);
-                    break;
-                  }
+              //     case 5: {
+              //       // 绘制评语
+              //       const coords = {
+              //         x: parseFloat((drawItem.x * zoomScale + offsetX).toFixed(2)),
+              //         y: parseFloat((drawItem.y * zoomScale + offsetY).toFixed(2))
+              //       };
+              //       this.DrawText(coords.x, coords.y, drawItem.text);
+              //       break;
+              //     }
                   
-                  default:
-                    console.warn('未知的绘图类型:', drawItem.drawType);
-                }
-              }
+              //     default:
+              //       console.warn('未知的绘图类型:', drawItem.drawType);
+              //   }
+              // }
 
               
             }
@@ -805,6 +931,76 @@ export default {
         }
     },
 
+    //寻找对象在数组中的索引
+    FindBlockIndex(targetObj,arr)
+    {
+       // 累计高度,用来确定对象位于哪个区间
+      let cumulativeHeight = 0;
+      
+      for (let i = 0; i < arr.length; i++) {
+        const item = arr[i];
+        if(targetObj.drawType==4)
+        {
+          //画笔判断
+          if (targetObj.drawlineData[0].y >= cumulativeHeight && targetObj.drawlineData[0].y < cumulativeHeight + item.h) {
+            
+            return i; // 返回匹配的索引
+          }
+        }
+        else
+        {
+           // 检查目标对象的y值是否在当前区间的范围内
+          if (targetObj.y >= cumulativeHeight && targetObj.y < cumulativeHeight + item.h) {
+            return i; // 返回匹配的索引
+          }
+        }
+       
+        
+        // 累计高度,为下一个区间的起始位置
+        cumulativeHeight += item.h;
+      }
+      
+      return -1; // 如果没有找到匹配的区间,返回-1
+    },
+
+    //寻找对象在数组中的对象
+    FindTargetObj(targetObj,arr)
+    {
+       // 累计高度,用来确定对象位于哪个区间
+      let cumulativeHeight = 0;
+      
+      for (let i = 0; i < arr.length; i++) {
+        const item = arr[i];
+        
+       
+        // 判断画笔
+        if(targetObj.drawType==4)
+        {
+          
+          if (targetObj.drawlineData[0].y >= cumulativeHeight && targetObj.drawlineData[0].y < cumulativeHeight + item.h) {
+            
+            targetObj.drawlineData.forEach((item,index) => {
+              item.y=item.y-cumulativeHeight;
+            });
+          }
+        }
+        else
+        {
+           // 检查目标对象的y值是否在当前区间的范围内
+          if (targetObj.y >= cumulativeHeight && targetObj.y < cumulativeHeight + item.h) {
+            targetObj.y=targetObj.y-cumulativeHeight;
+          
+          }
+        }
+
+        
+        // 累计高度,为下一个区间的起始位置
+        cumulativeHeight += item.h;
+      }
+      
+      return targetObj;  
+    },
+
 
 
     //初始数据加载
@@ -915,11 +1111,17 @@ export default {
     ImageInfoChange(){
       let {width,height} = this.paperImgInfo;
       // console.log("打印纸张试卷的图片宽高",this.paperImgInfo);
-      let imgDom = document.getElementById(`imgContainer${this.imageIndex}`);
+      // let imgDom = document.getElementById(`imgContainer${this.imageIndex}`);
+      // imgDom.style.width = width * this.zoomRate*this.scale + 'px';
+      // imgDom.style.height = height * this.zoomRate*this.scale + 'px';
+      // imgDom.style.left = this.position.x + 'px';
+      // imgDom.style.top = this.position.y + 'px';
+
+      let imgDom = this.$refs.imgContainer;
       imgDom.style.width = width * this.zoomRate*this.scale + 'px';
       imgDom.style.height = height * this.zoomRate*this.scale + 'px';
       imgDom.style.left = this.position.x + 'px';
-      imgDom.style.top = this.position.y + 'px';
+      imgDom.style.top = this.position.y + 'px';  
     },
 
     //图片加载完成后绘制图片
@@ -1114,7 +1316,7 @@ export default {
                 text = item.displayName.toString();
                 textWidth = ctx.measureText(text).width;
               }else{
-                textWidth = 2 * fontSize*this.zoomRate*this.scale; ;
+                textWidth = 2 * fontSize*this.zoomRate*this.scale; 
               }
               const underlineY = obj.y + 50*this.zoomRate * this.scale ; // 可根据需要调整下划线位置
               const startX = obj.x +35*this.zoomRate*this.scale;
@@ -1511,8 +1713,15 @@ export default {
     updateZoomAndPaperInfo() {
       // this.zoomRate = this.containerHeight / this.paperImgInfo.height;//
       // 计算基于宽度和高度的缩放率
-      const widthZoomRate = this.containerWidth / this.paperImgInfo.width;
-      const heightZoomRate = this.containerHeight / this.paperImgInfo.height;
+      let widthZoomRate = 1;
+      let heightZoomRate = 1;
+      if(this.rotateDeg=='-90'){//旋转90度 学生报告册使用
+        widthZoomRate = this.containerWidth / this.paperImgInfo.height;
+        heightZoomRate = (this.containerHeight - 40) / this.paperImgInfo.width;//减掉报告册底边距40px
+      }else{
+        widthZoomRate = this.containerWidth / this.paperImgInfo.width;
+        heightZoomRate = this.containerHeight / this.paperImgInfo.height;
+      }
       
       // 选择较小的缩放率作为基准,确保图像完整显示在容器内
       this.zoomRate = Math.min(widthZoomRate, heightZoomRate);
@@ -1536,7 +1745,11 @@ export default {
       console.log("开始居中画布",this.containerWidth,this.containerHeight);
       console.log("画布尺寸",this.canvasInfo.width,this.canvasInfo.height);
       this.position.x = (this.containerWidth - this.canvasInfo.width) / 2;
-      this.position.y = (this.containerHeight - this.canvasInfo.height) / 2;
+      if(this.rotateDeg=='-90'){//学生报告册使用
+        this.position.y = (this.containerHeight - this.canvasInfo.height) / 2 - 40;
+      }else{
+        this.position.y = (this.containerHeight - this.canvasInfo.height) / 2;
+      }
       this.$refs.paperCanvas.style.left = `${this.position.x}px`;
       this.$refs.paperCanvas.style.top = `${this.position.y}px`;
       console.log("中心化画布打印画布位置",this.position.x,this.position.y);
@@ -1630,6 +1843,7 @@ export default {
 
     // 鼠标滚轮事件
     onWheel(event) {
+      if(!this.isWheel) return false
       event.preventDefault();
       // 计算新的缩放比例
       const delta = event.deltaY < 0 ? 1 : -1;

+ 10 - 4
src/components/StudentPaper.vue

@@ -61,7 +61,7 @@
 <script>
 
 import PaperImage from '@/components/PaperImage.vue';//学生试卷组件
-
+import {getApiName} from '@/utils/common';
 export default {
     name: 'StudentPaper',//学生试卷组件
     components:
@@ -192,17 +192,17 @@ export default {
         if(this.paperInfo.examId!='' && this.paperInfo.subjectCode!=null)
         {
           this.isLoading=true;
-          this.$api.reportStudent.findStudentCard(this.paperInfo).then(res=>{
+          this.$api.reportStudent[getApiName()].findStudentCard(this.paperInfo).then(res=>{
             console.log("打印学生试卷详情信息",res);
             
             
 
-            if(res.code==200)
+            if(res.code==200 && res.data)
             {
               this.paperImageList=res?.data?.pageVOS || [];
               const studentCode =  res?.data?.studentCode ?? '';
               this.studentCode = studentCode ?`【${studentCode}】`:'';
-              this.usedCardType=res.data.usedCardType;
+              this.usedCardType=res?.data?.usedCardType ?? 2;
               // 重置索引并更新当前试卷数据
               // this.currentIndex = 0;
               this.UpdateCurrentPaperData();
@@ -241,7 +241,13 @@ export default {
             }
             else
             {
+              this.currentPaperUrl="";//清空试卷地址
+              this.currentDrawData=[];//清空试卷答题数据
+              this.paperImageList = [];
+              this.currentIndex = 0;
+              this.questionList = [];
               this.studentCode = '';
+              this.questionTableHeight='';
               this.$nextTick(() => {
                 this.isLoading=false;
               });

+ 4 - 1
src/http/api.js

@@ -1,6 +1,9 @@
 import user from './api/user'//个人中心 相关接口
 import reportStudent from "./api/reportStudent";
+import personalProfile from "./api/personalProfile";
+
 export default {
     user,  
-    reportStudent
+    reportStudent,
+    personalProfile,
 }

+ 1 - 1
src/http/api/errorQuestion.js

@@ -8,4 +8,4 @@ function url(url) {
 export const queryStudentErrorQuestion = (data) => get(url("/api/v1/studentError/queryStudentErrorQuestion"), data);
 export const markStudentErrorQuestion = (data) => get(url("/api/v1/studentError/markStudentErrorQuestion"), data);
 export const downloadStudentErrorQuestion = (data) => downLoadByBlob(url("/api/v1/studentError/downloadStudentErrorQuestion"), data);
-
+export const findStudentCard = (data) => get(url("/api/v1/studentData/findStudentCard"), data);

+ 35 - 0
src/http/api/personalProfile.js

@@ -0,0 +1,35 @@
+import { get, post,downLoadByBlob} from "../common/http";
+import base from "../common/base";
+
+const report = {
+    // 学生-历次变化
+    examScoreList(data){
+        return get(`${base.prefix}/api/v1/studentPinpoint/examScoreList`,data);
+    } ,
+    // 学生-历次考试知识点追踪
+    examKnowledgePointTrack(data){
+        return post(`${base.prefix}/api/v1/studentPinpoint/examKnowledgePointTrack`,data);
+    },
+    // 知识点图谱
+    studentKnowledgeGraph(data){
+        return post(`${base.prefix}/api/v1/studentPinpoint/studentKnowledgeGraph`,data);
+    },
+    // 知识点树
+    studentKnowledgeDataTree(data){
+        return post(`${base.prefix}/api/v1/studentPinpoint/studentKnowledgeDataTree`,data);
+    },
+    // 历次变化
+    knowledgeExamPrevious(data){
+        return post(`${base.prefix}/api/v1/studentPinpoint/knowledgeExamPrevious`,data);
+    },
+    // 推送试题列表
+    pushQuestionList(data){
+        return get(`${base.prefix}/api/v1/studentPinpoint/pushQuestionList`,data);
+    },
+    // 导出精准提升试题
+    downloadPushQuestion(data){
+        return downLoadByBlob(`${base.prefix}/api/v1/studentPinpoint/downloadPushQuestion`,data);
+    },
+};
+
+export default report;

+ 11 - 3
src/http/api/reportStudent.js

@@ -79,6 +79,13 @@ const jointSchoolReport = {
             data
         );
     },
+    //学生端查询单科-知识点分层分析表(联考)
+    queryStudentKnowLedgeLayering(data) {
+        return post(
+            base.prefix + "/api/v1/studentData/queryStudentKnowLedgeLayering",
+            data
+        );
+    },
 };
 //单校接口
 const schoolReport = {
@@ -160,6 +167,7 @@ const schoolReport = {
         );
     },
 };
-const schoolType = sessionStorage.getItem('schoolType') ?? '1';//1:单校  2:联校
-const report = schoolType=='1'?schoolReport:jointSchoolReport;
-export default { ...report };
+// const schoolType = sessionStorage.getItem('schoolType') ?? '1';//1:单校  2:联校
+// const report = schoolType=='1'?schoolReport:jointSchoolReport;
+// export default { ...report };
+export default { schoolReport,jointSchoolReport };

+ 5 - 1
src/main.js

@@ -11,13 +11,17 @@ import { debounce } from "./utils/common";
 import * as echarts from "echarts";
 
 import "@/styles/element-variables.scss";
-// import "vxe-table/lib/style.css";
+// 引入vxe-table
+import VXETable from 'vxe-table';
+import 'vxe-table/lib/style.css';
+
 import "@/styles/common.scss";
 
 import "font-awesome/css/font-awesome.min.css";
 import './styles/XueKeWangQuestionStyles.css'
 import './assets/font2/iconfont.css'
 
+Vue.use(VXETable);
 // 解决el-radio报错
 Vue.directive('removeAriaHidden', {
   bind(el, binding) {

+ 1 - 1
src/plugins/lib-flexible/flexible.js

@@ -1,4 +1,4 @@
-;(function(win, lib) {
+(function(win, lib) {
     var doc = win.document;
     var docEl = doc.documentElement;
     var metaEl = doc.querySelector('meta[name="viewport"]');

+ 55 - 39
src/router/index.js

@@ -9,38 +9,38 @@ Router.prototype.push = function push(location) {
 };
 // 其他路由
 let constantRoutes = [
-  {
-    path: "/",
-    component: () => import("@/views/login/login.vue"),
-    meta: {
-      title: "login",
-    },
-  },
-  {
-    path: "/login",
-    component: () => import("@/views/login/login_ruoyan.vue"),
-    meta: {
-      title: "login",
+    {
+        path: "/",
+        component: () => import("@/views/login/login.vue"),
+        meta: {
+            title: "login",
+        },
     },
-  },
-  // 个人中心
-  {
-    path: "/userInfo",
-    redirect: "/userInfo/personInfo",
-    component: () => import("@/views/userInfo/index"),
-    meta: {
-      title: "个人中心",
+    {
+        path: "/login",
+        component: () => import("@/views/login/login_ruoyan.vue"),
+        meta: {
+            title: "login",
+        },
     },
-    children: [
-      {
-        path: "personInfo",
-        component: () => import("@/views/userInfo/personInfo"),
+    // 个人中心
+    {
+        path: "/userInfo",
+        redirect: "/userInfo/personInfo",
+        component: () => import("@/views/userInfo/index"),
         meta: {
-          title: "个人信息",
+            title: "个人中心",
         },
-      },
-    ],
-  },
+        children: [
+            {
+                path: "personInfo",
+                component: () => import("@/views/userInfo/personInfo"),
+                meta: {
+                    title: "个人信息",
+                },
+            },
+        ],
+    },
 ];
 // 学生端分析报告
 let studentAnalysisReport = {
@@ -49,12 +49,19 @@ let studentAnalysisReport = {
     component: () => import("@/views/analysisReport/index"),
     children: [
         {
-            path: "/studentAnalysisReport/list", // 报告列表
+            path: "/jointStudentAnalysisReport/list", // 报告列表
             meta: {
                 title: "分析报告",
             },
             component: () => import("@/views/analysisReport/studentPage/list/mainPage"),
         },
+        {
+            path: "/studentAnalysisReport/list", // 报告列表
+            meta: {
+                title: "校考分析报告",
+            },
+            component: () => import("@/views/analysisReport/studentPage/list/mainPage"),
+        },
         {
             name: "reportDetails",
             path: "/studentAnalysisReport/reportDetails",
@@ -67,11 +74,25 @@ let studentAnalysisReport = {
                         title: "学生分析",
                     },
                 },
+                {
+                    path: "studentReport",
+                    meta: {
+                        title: "学生报告册",
+                    },
+                    component: () => import("@/views/analysisReport/studentPage/downloadPdf/studentReport"),
+                },
                 {
                     path: "personalWrongQuestions",
                     component: () => import("@/views/analysisReport/wrongQuestion/index"),
                     meta: {
-                        title: "个性化错题",
+                        title: "举一反三",
+                    },
+                },
+                {
+                    path: "personalProfile",
+                    component: () => import("@/views/analysisReport/personalProfile/index"),
+                    meta: {
+                        title: "个人画像",
                     },
                 },
             ],
@@ -101,19 +122,16 @@ function addAnalyticsScript() {
 
     // 移除已存在的统计脚本
     let existingScript = document.getElementById('_bxtj');
-    // console.log('移除已存在的统计脚本...',existingScript);
     while (existingScript) {
         existingScript.remove();
         existingScript = document.getElementById('_bxtj');
     }
     // 移除已存在的统计脚本
     let existingScriptFrame = document.getElementById('_bxtjiframe');
-    // console.log('移除已存在的统计脚本...',existingScriptFrame);
     while (existingScriptFrame) {
         existingScriptFrame.remove();
         existingScriptFrame = document.getElementById('_bxtjiframe');
     }
-    console.log('添加统计分析脚本...');
     // 等待DOM加载完成
     setTimeout(() => {
         var _s = document.createElement('script');
@@ -126,7 +144,6 @@ function addAnalyticsScript() {
         const currentUser = userInfo.loginName || ''; // 当前用户名
         const subSiteName = userInfo.schoolName || '';
         const mySiteId = localStorage.getItem("user_schoolWebSiteId") || ''; // 你的站点ID
-        console.log("当前学校站点id", userInfo, mySiteId);
         if (mySiteId != '') {
             // 构建URL(按照原始代码格式)
             let src = `https://bjedures.bjedu.cn/bjjw_logdb/bxlog.js?user=${currentUser}&id=${mySiteId}`;
@@ -136,16 +153,15 @@ function addAnalyticsScript() {
             _s.setAttribute('src', src);
             var body = document.getElementsByTagName('body');
             body[0].appendChild(_s);
-            console.log('统计分析脚本已添加到body中。');
         }
     }, 1000);//1秒后添加
 }
 let router = createRouter();
 router.beforeEach((to, from, next) => {
-  if (to.meta.title) {
-    document.title = "大数据精准教学诊断平台-" + to.meta.title;
-  }
-  next();
+    if (to.meta.title) {
+        document.title = "大数据精准教学诊断平台-" + to.meta.title;
+    }
+    next();
 });
 // 路由后置守卫:添加统计脚本
 router.afterEach(() => {

+ 6 - 1
src/store/modules/question.js

@@ -1,11 +1,16 @@
 const state = {
-    isVariation: -1
+    isVariation: -1,
+    canDownloanBtnClick: true
 }
 
 const mutations = {
     SET_VARIANTION: (state, data) => {
         state.isVariation = data
     },
+
+    setCanDownloanBtnClick: (state, data) => {
+        state.canDownloanBtnClick = data
+    }
 }
 
 export default {

Разлика између датотеке није приказан због своје велике величине
+ 1045 - 92
src/styles/report.scss


+ 159 - 93
src/utils/common.js

@@ -1,3 +1,5 @@
+import { MessageBox, Message } from 'element-ui';
+
 // 写一个防抖函数
 export function debounce(fn, delay) {
     let timer = null
@@ -36,7 +38,7 @@ export function throttle(fn, delay) {
 //         arrDPI[1] = window.screen.deviceYDPI;
 
 //     } else {
-        
+
 
 //         var tmpNode = document.createElement("DIV");
 
@@ -61,7 +63,7 @@ export function throttle(fn, delay) {
  */
 function getDPI() {
     return window.devicePixelRatio * 96; // 通常情况下,1英寸 = 96像素
-  }
+}
 
 
 //毫米转px
@@ -101,7 +103,7 @@ export function fixNumber(num) {
 
 export function pxToMm(px) {
 
-   if (px) {
+    if (px) {
         // 获取设备像素比
         const devicePixelRatio = window.devicePixelRatio || 1;
         // 标准DPI为96,考虑设备像素比进行调整
@@ -132,20 +134,18 @@ export function pxToMm(px) {
 
 // 保留整数 小数 四舍五入
 export function mmToPx(num) {
-    if(num)
-    {
+    if (num) {
         // console.log("num",num);
         // console.log("转换后",Math.round(1754/297*num));
         // let scale=1754/297;
         // let result=(scale*num).toFixed(4);
         // console.log("scale",scale,result);
-        let scale=1754/297;
-        return  parseFloat((scale*num).toFixed(4)); //高 150dpi 1754  96dpi:1120         1754*2480
+        let scale = 1754 / 297;
+        return parseFloat((scale * num).toFixed(4)); //高 150dpi 1754  96dpi:1120         1754*2480
         // return Math.round(1754/297*num)
     }
-    else
-    {
-        console.log("num",num);
+    else {
+        console.log("num", num);
         return 0;
     }
 }
@@ -168,96 +168,162 @@ export function mmToPx(num) {
 //     }
 // }
 
- //获取显示的状态
- export function getStateName(scannedStatus,studentStatus)
- {
-     // console.log(scannedStatus,studentStatus);
-     let result='正常';
-     if(scannedStatus==1)
-     {
-         result='正常'
-
-     }else if(scannedStatus==2)
-     {
-         result='缺考'
-
-     }else if(scannedStatus==3)
-     {
-         //异常分类
-         if(studentStatus==2)
-         {
-             result='定位异常'
-
-         }else if(studentStatus==3)
-         {
-             result='考号异常'
-
-         }else if(studentStatus==4)
-         {
-             result='客观题异常'
-
-         }else if(studentStatus==5)
-         {
-             result='选做题异常'
-
-         }else if(studentStatus==6)
-         {
-             result='空白卷异常'
-
-         }else if(studentStatus==7)
-         {
-             result='考号重复'
-         }
-     }
-     return result;
- }
-
-
- //获取百分比  type  progress 表示进度条使用
- export function getPercentage(num1,num2,type)
- {
-   
-    let result=0;
-    let percentage=0;
-    if(num1 && num2)
-    {
-        percentage=(parseFloat(num1)/parseFloat(num2)).toFixed(2);
+//获取显示的状态
+export function getStateName(scannedStatus, studentStatus) {
+    // console.log(scannedStatus,studentStatus);
+    let result = '正常';
+    if (scannedStatus == 1) {
+        result = '正常'
+
+    } else if (scannedStatus == 2) {
+        result = '缺考'
+
+    } else if (scannedStatus == 3) {
+        //异常分类
+        if (studentStatus == 2) {
+            result = '定位异常'
+
+        } else if (studentStatus == 3) {
+            result = '考号异常'
+
+        } else if (studentStatus == 4) {
+            result = '客观题异常'
+
+        } else if (studentStatus == 5) {
+            result = '选做题异常'
+
+        } else if (studentStatus == 6) {
+            result = '空白卷异常'
+
+        } else if (studentStatus == 7) {
+            result = '考号重复'
+        }
+    }
+    return result;
+}
+
+
+//获取百分比  type  progress 表示进度条使用
+export function getPercentage(num1, num2, type) {
+
+    let result = 0;
+    let percentage = 0;
+    if (num1 && num2) {
+        percentage = (parseFloat(num1) / parseFloat(num2)).toFixed(2);
 
     }
 
-    result= Math.round(percentage*100);
+    result = Math.round(percentage * 100);
     // console.log("num1,num2",num1,num2,percentage,result);
-    if(type=='progress')
-    {
-        
+    if (type == 'progress') {
+
         //进度条的值不能超100%
-        if(result>100)
-        {
-        result=100;
+        if (result > 100) {
+            result = 100;
         }
-        
+
     }
-    if(result<0)
-    {
-        result=0;
+    if (result < 0) {
+        result = 0;
     }
-  
-//    console.log("打印结果",result);
-   return result;
- }
-
-  //获取考试背景颜色值
-  export function getExamBackgroundColor(type)
-  {
-    const examTypesColor={
-        "1": {color:'#F56C6C',background:'#F56C6C'},//期末
-        "2": {color:'#3ba272',background:'#3ba272'},//期中
-        "3": {color:'#ea7acb',background:'#ea7acb'},//模拟
-        "4": {color:'#fac858',background:'#fac858'},//月考
-        "5": {color:'#995fb3',background:'#995fb3'},//周测
-        "6": {color:'#72C0DD',background:'#72C0DD'},//平时
+
+    //    console.log("打印结果",result);
+    return result;
+}
+
+//获取考试背景颜色值
+export function getExamBackgroundColor(type) {
+    const examTypesColor = {
+        "1": { color: '#F56C6C', background: '#F56C6C' },//期末
+        "2": { color: '#3ba272', background: '#3ba272' },//期中
+        "3": { color: '#ea7acb', background: '#ea7acb' },//模拟
+        "4": { color: '#fac858', background: '#fac858' },//月考
+        "5": { color: '#995fb3', background: '#995fb3' },//周测
+        "6": { color: '#72C0DD', background: '#72C0DD' },//平时
     };
-    const result=examTypesColor[type].background;
-    
+    const result = examTypesColor[type].background;
+
     return result;
-  }
+}
+//获取当前接口地址名称
+export function getApiName() {
+    const schoolType = sessionStorage.getItem('schoolType') ?? 1;//1:单校  2:联校
+    return schoolType == 1 ? 'schoolReport' : 'jointSchoolReport'
+}
+
+
+/**
+* 显示确认对话框
+* @param {string} title - 对话框标题
+* @param {function} successCallback - 确认按钮回调函数
+* @param {function|string} [cancelCallback='已取消操作'] - 取消按钮回调函数或提示信息
+*/
+export function confirm(title, successCallback, cancelCallback = '已取消操作') {
+    MessageBox.confirm(title, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        customClass: 'page_dialog',
+    }).then(successCallback).catch(() => {
+        const type = typeof cancelCallback;
+        if (type === 'function') {
+            cancelCallback();
+            return;
+        } else {
+            info(cancelCallback);
+        }
+    });
+}
+
+
+
+/**
+ * 显示成功消息
+ * @param {string} message - 消息内容
+*/
+export function success(message) {
+    Message({
+        message,
+        type: 'success',
+        duration: 2000
+    });
+}
+
+
+/**
+ * 显示错误消息
+ * @param {string} message - 消息内容
+*/
+export function error(message) {
+    Message({
+        message,
+        type: 'error',
+        duration: 2000
+    });
+}
+
+
+/**
+ * 显示信息消息
+ * @param {string} message - 消息内容
+*/
+export function info(message) {
+    Message({
+        message,
+        type: 'info',
+        duration: 2000
+    });
+}
+
+
+/**
+ * 显示警告消息
+ * @param {string} message - 消息内容
+*/
+export function warning(message) {
+    Message({
+        message,
+        type: 'warning',
+        duration: 2000
+    });
+}

+ 1 - 1
src/utils/scan.js

@@ -93,7 +93,7 @@ var wsc2={
                     on_offline();//处理离线逻辑方法
                 },self.interval_close);
             },self.interval_ping);//每隔 self.interval_ping 毫秒
-        };
+        }
         //离线处理逻辑  
         function on_offline(){
             

+ 3352 - 0
src/utils/turn.js

@@ -0,0 +1,3352 @@
+/**
+ * turn.js 4th release
+ * turnjs.com
+ * turnjs.com/license.txt
+ *
+ * Copyright (C) 2012 Emmanuel Garcia
+ * All rights reserved
+ **/
+
+(function($) {
+
+'use strict';
+
+var has3d,
+  
+  hasRot,
+
+  vendor = '',
+  
+  version = '4.1.0',
+
+  PI = Math.PI,
+
+  A90 = PI/2,
+
+  isTouch = 'ontouchstart' in window,
+
+  mouseEvents = (isTouch) ?
+    {
+      down: 'touchstart',
+      move: 'touchmove',
+      up: 'touchend',
+      over: 'touchstart',
+      out: 'touchend'
+    }
+    :
+    {
+      down: 'mousedown',
+      move: 'mousemove',
+      up: 'mouseup',
+      over: 'mouseover',
+      out: 'mouseout'
+    },
+
+  // Contansts used for each corner
+  //   | tl * tr |
+  // l | *     * | r
+  //   | bl * br |
+
+  corners = {
+    backward: ['bl', 'tl'],
+    forward: ['br', 'tr'],
+    all: ['tl', 'bl', 'tr', 'br', 'l', 'r']
+  },
+
+  // Display values  单双页显示
+
+  displays = ['single', 'double'],
+
+  // Direction values  表示方向的值
+
+  directions = ['ltr', 'rtl'],
+
+  // Default options 默认属性
+
+  turnOptions = {
+
+    // Enables hardware acceleration  是否允许硬件加速
+
+    acceleration: true,
+
+    // Display  双页显示
+
+    display: 'double',
+
+    // Duration of transition in milliseconds  翻页速度
+
+    duration: 1000,
+
+    // First page  首页页码
+
+    page: 1,
+    
+    // Enables gradients  是否允许渐变
+
+    gradients: true,
+
+    // Corners used when turning the page  卷边效果
+
+    turnCorners: 'bl,br',
+
+    // Events
+
+    when: null
+  },
+
+  flipOptions = {
+
+    // Size of the active zone of each corner  卷边大小
+    
+    cornerSize: 300
+    
+  },
+
+  // Number of pages in the DOM, minimum value: 6
+
+  pagesInDOM = 6,
+  
+
+turnMethods = {
+
+  // Singleton constructor
+  // $('#selector').turn([options]);
+
+  init: function(options) {
+
+    // Define constants
+    
+    has3d = 'WebKitCSSMatrix' in window || 'MozPerspective' in document.body.style;
+    hasRot = rotationAvailable();
+    vendor = getPrefix();
+
+    var i, that = this, pageNum = 0, data = this.data(), ch = this.children();
+
+    // Set initial configuration
+
+    options = $.extend({
+      width: this.width(),
+      height: this.height(),
+      direction: this.attr('dir') || this.css('direction') || 'ltr'
+    }, turnOptions, options);
+
+    data.opts = options;
+    data.pageObjs = {};
+    data.pages = {};
+    data.pageWrap = {};
+    data.pageZoom = {};
+    data.pagePlace = {};
+    data.pageMv = [];
+    data.zoom = 1;
+    data.totalPages = options.pages || 0;
+    data.eventHandlers = {
+      touchStart: $.proxy(turnMethods._touchStart, this),
+      touchMove: $.proxy(turnMethods._touchMove, this),
+      touchEnd: $.proxy(turnMethods._touchEnd, this),
+      start: $.proxy(turnMethods._eventStart, this)
+    };
+
+
+
+    // Add event listeners
+
+    if (options.when)
+      for (i in options.when)
+        if (has(i, options.when))
+          this.bind(i, options.when[i]);
+
+    // Set the css
+
+    this.css({position: 'relative', width: options.width, height: options.height});
+
+    // Set the initial display
+
+    this.turn('display', options.display);
+
+    // Set the direction
+
+    if (options.direction!=='')
+      this.turn('direction', options.direction);
+    
+    // Prevent blue screen problems of switching to hardware acceleration mode
+    // By forcing hardware acceleration for ever
+
+    if (has3d && !isTouch && options.acceleration)
+      this.transform(translate(0, 0, true));
+
+    // Add pages from the DOM
+
+    for (i = 0; i<ch.length; i++) {
+      if ($(ch[i]).attr('ignore')!='1') {
+        this.turn('addPage', ch[i], ++pageNum);
+      }
+    }
+
+    // Event listeners
+
+    $(this).bind(mouseEvents.down, data.eventHandlers.touchStart).
+      bind('end', turnMethods._eventEnd).
+      bind('pressed', turnMethods._eventPressed).
+      bind('released', turnMethods._eventReleased).
+      bind('flip', turnMethods._flip);
+
+    $(this).parent().bind('start', data.eventHandlers.start);
+
+    $(document).bind(mouseEvents.move, data.eventHandlers.touchMove).
+      bind(mouseEvents.up, data.eventHandlers.touchEnd);
+
+    // Set the initial page
+
+    this.turn('page', options.page);
+
+    // This flipbook is ready
+
+    data.done = true;
+
+    return this;
+  },
+
+  // Adds a page from external data
+
+  addPage: function(element, page) {
+
+    var currentPage,
+      className,
+      incPages = false,
+      data = this.data(),
+      lastPage = data.totalPages+1;
+
+    if (data.destroying)
+      return false;
+
+    // Read the page number from the className of `element` - format: p[0-9]+
+
+    if ((currentPage = /\bp([0-9]+)\b/.exec($(element).attr('class'))))
+      page = parseInt(currentPage[1], 10);
+
+    if (page) {
+      
+      if (page==lastPage)
+        incPages = true;
+      else if (page>lastPage)
+        throw turnError('Page "'+page+'" cannot be inserted');
+
+    } else {
+      
+      page = lastPage;
+      incPages = true;
+
+    }
+
+    if (page>=1 && page<=lastPage) {
+
+      if (data.display=='double')
+        className = (page%2) ? ' odd' : ' even';
+      else
+        className = '';
+
+      // Stop animations
+      if (data.done)
+        this.turn('stop');
+
+      // Move pages if it's necessary
+      if (page in data.pageObjs)
+        turnMethods._movePages.call(this, page, 1);
+
+      // Increase the number of pages
+      if (incPages)
+        data.totalPages = lastPage;
+
+      // Add element
+      data.pageObjs[page] = $(element).
+        css({'float': 'left'}).
+        addClass('page p' + page + className);
+
+      if (!hasHardPage() &&  data.pageObjs[page].hasClass('hard')) {
+        data.pageObjs[page].removeClass('hard');
+      }
+
+      // Add page
+      turnMethods._addPage.call(this, page);
+
+      // Remove pages out of range
+      turnMethods._removeFromDOM.call(this);
+    }
+
+    return this;
+  },
+
+  // Adds a page
+
+  _addPage: function(page) {
+    
+    var data = this.data(),
+      element = data.pageObjs[page];
+
+    if (element)
+      if (turnMethods._necessPage.call(this, page)) {
+
+        if (!data.pageWrap[page]) {
+
+          // Wrapper
+          data.pageWrap[page] = $('<div/>',
+            {'class': 'page-wrapper',
+              page: page,
+              css: {position: 'absolute',
+              overflow: 'hidden'}});
+
+          // Append to this flipbook
+          this.append(data.pageWrap[page]);
+
+          if (!data.pagePlace[page]) {
+            
+            data.pagePlace[page] = page;
+            // Move `pageObjs[page]` to wrapper
+            data.pageObjs[page].appendTo(data.pageWrap[page]);
+          
+         }
+
+          // Set the size of the page
+          var prop = turnMethods._pageSize.call(this, page, true);
+          element.css({width: prop.width, height: prop.height});
+          data.pageWrap[page].css(prop);
+
+        }
+
+        if (data.pagePlace[page] == page) {
+
+         // If the page isn't in another place, create the flip effect
+          turnMethods._makeFlip.call(this, page);
+
+      }
+        
+      } else {
+
+        // Place
+        data.pagePlace[page] = 0;
+
+        // Remove element from the DOM
+        if (data.pageObjs[page])
+          data.pageObjs[page].remove();
+
+      }
+
+  },
+
+  // Checks if a page is in memory
+  
+  hasPage: function(page) {
+
+    return has(page, this.data().pageObjs);
+  
+  },
+
+  // Centers the flipbook
+
+  center: function(page) {
+    
+    var data = this.data(),
+      size = $(this).turn('size'),
+      left = 0;
+
+    if (!data.noCenter) {
+      if (data.display=='double') {
+        var view = this.turn('view', page || data.tpage || data.page);
+
+        if (data.direction=='ltr') {
+          if (!view[0])
+            left -= size.width/4;
+          else if (!view[1])
+            left += size.width/4;
+        } else {
+          if (!view[0])
+            left += size.width/4;
+          else if (!view[1])
+            left -= size.width/4;
+        }
+      
+      }
+
+      $(this).css({marginLeft: left});
+    }
+
+    return this;
+
+  },
+
+  // Destroys the flipbook
+
+  destroy: function () {
+
+    var page,
+      flipbook = this,
+      data = this.data(),
+      events = [
+        'end', 'first', 'flip', 'last', 'pressed',
+        'released', 'start', 'turning', 'turned',
+        'zooming', 'missing'];
+
+    if (trigger('destroying', this)=='prevented')
+      return;
+
+    data.destroying = true;
+
+    $.each(events, function(index, eventName) {
+      flipbook.unbind(eventName);
+    });
+
+    this.parent().unbind('start', data.eventHandlers.start);
+
+    $(document).unbind(mouseEvents.move, data.eventHandlers.touchMove).
+      unbind(mouseEvents.up, data.eventHandlers.touchEnd);
+    
+    while (data.totalPages!==0) {
+      this.turn('removePage', data.totalPages);
+    }
+
+    if (data.fparent)
+      data.fparent.remove();
+
+    if (data.shadow)
+      data.shadow.remove();
+
+    this.removeData();
+    data = null;
+
+    return this;
+
+  },
+
+  // Checks if this element is a flipbook
+
+  is: function() {
+
+    return typeof(this.data().pages)=='object';
+
+  },
+
+  // Sets and gets the zoom value
+
+  zoom: function(newZoom) {
+    
+    var data = this.data();
+
+    if (typeof(newZoom)=='number') {
+
+      if (newZoom<0.001 || newZoom>100)
+        throw turnError(newZoom+ ' is not a value for zoom');
+      
+      if (trigger('zooming', this, [newZoom, data.zoom])=='prevented')
+        return this;
+      
+      var size = this.turn('size'),
+        currentView = this.turn('view'),
+        iz = 1/data.zoom,
+        newWidth = Math.round(size.width * iz * newZoom),
+        newHeight = Math.round(size.height * iz * newZoom);
+    
+      data.zoom = newZoom;
+
+      $(this).turn('stop').
+        turn('size', newWidth, newHeight);
+        /*.
+        css({marginTop: size.height * iz / 2 - newHeight / 2});*/
+
+      if (data.opts.autoCenter)
+        this.turn('center');
+      /*else
+        $(this).css({marginLeft: size.width * iz / 2 - newWidth / 2});*/
+
+      turnMethods._updateShadow.call(this);
+
+      for (var i = 0; i<currentView.length; i++) {
+        if (currentView[i] && data.pageZoom[currentView[i]]!=data.zoom) {
+  
+          this.trigger('zoomed',[
+            currentView[i],
+            currentView,
+            data.pageZoom[currentView[i]],
+            data.zoom]);
+
+          data.pageZoom[currentView[i]] = data.zoom;
+      }
+    }
+
+      return this;
+
+    } else
+      return data.zoom;
+
+  },
+
+  // Gets the size of a page
+
+  _pageSize: function(page, position) {
+
+    var data = this.data(),
+      prop = {};
+
+    if (data.display=='single') {
+
+      prop.width = this.width();
+      prop.height = this.height();
+
+      if (position) {
+        prop.top = 0;
+        prop.left = 0;
+        prop.right = 'auto';
+      }
+
+    } else {
+
+      var pageWidth = this.width()/2,
+        pageHeight = this.height();
+
+      if (data.pageObjs[page].hasClass('own-size')) {
+        prop.width = data.pageObjs[page].width();
+        prop.height = data.pageObjs[page].height();
+      } else {
+        prop.width = pageWidth;
+        prop.height = pageHeight;
+      }
+      
+      if (position) {
+        var odd = page%2;
+        prop.top = (pageHeight-prop.height)/2;
+
+        if (data.direction=='ltr') {
+          
+          prop[(odd) ? 'right' : 'left'] = pageWidth-prop.width;
+          prop[(odd) ? 'left' : 'right'] = 'auto';
+
+        } else {
+          
+          prop[(odd) ? 'left' : 'right'] = pageWidth-prop.width;
+          prop[(odd) ? 'right' : 'left'] = 'auto';
+
+        }
+      
+      }
+    }
+
+    return prop;
+
+  },
+
+  // Prepares the flip effect for a page
+
+  _makeFlip: function(page) {
+
+    var data = this.data();
+
+    if (!data.pages[page] && data.pagePlace[page]==page) {
+      
+      var single = data.display=='single',
+        odd = page%2;
+
+      data.pages[page] = data.pageObjs[page].
+        css(turnMethods._pageSize.call(this, page)).
+        flip({
+          page: page,
+          next: (odd || single) ? page+1 : page-1,
+          turn: this
+        }).
+        flip('disable', data.disabled);
+
+        // Issue about z-index
+        turnMethods._setPageLoc.call(this, page);
+
+        data.pageZoom[page] = data.zoom;
+        
+    }
+
+    return data.pages[page];
+  },
+
+  // Makes pages within a range
+
+  _makeRange: function() {
+
+    var page, range,
+      data = this.data();
+
+    if (data.totalPages<1)
+      return;
+
+    range = this.turn('range');
+
+    for (page = range[0]; page<=range[1]; page++)
+      turnMethods._addPage.call(this, page);
+
+  },
+
+  // Returns a range of pages that should be in the DOM
+  // Example:
+  // - page in the current view, return true
+  // * page is in the range, return true
+  // Otherwise, return false
+  //
+  // 1 2-3 4-5 6-7 8-9 10-11 12-13
+  //   **  **  --   **  **
+
+  range: function(page) {
+
+    var remainingPages, left, right, view,
+      data = this.data();
+
+      page = page || data.tpage || data.page || 1;
+      view = turnMethods._view.call(this, page);
+
+      if (page<1 || page>data.totalPages)
+        throw turnError('"'+page+'" is not a valid page');
+
+    
+      view[1] = view[1] || view[0];
+      
+      if (view[0]>=1 && view[1]<=data.totalPages) {
+
+        remainingPages = Math.floor((pagesInDOM-2)/2);
+
+        if (data.totalPages-view[1] > view[0]) {
+          left = Math.min(view[0]-1, remainingPages);
+          right = 2*remainingPages-left;
+        } else {
+          right = Math.min(data.totalPages-view[1], remainingPages);
+          left = 2*remainingPages-right;
+        }
+
+      } else {
+        left = pagesInDOM-1;
+        right = pagesInDOM-1;
+      }
+
+      return [Math.max(1, view[0]-left),
+          Math.min(data.totalPages, view[1]+right)];
+
+  },
+
+  // Detects if a page is within the range of `pagesInDOM` from the current view
+
+  _necessPage: function(page) {
+    
+    if (page===0)
+      return true;
+
+    var range = this.turn('range');
+
+    return this.data().pageObjs[page].hasClass('fixed') ||
+      (page>=range[0] && page<=range[1]);
+    
+  },
+
+  // Releases memory by removing pages from the DOM
+
+  _removeFromDOM: function() {
+
+    var page, data = this.data();
+
+    for (page in data.pageWrap)
+      if (has(page, data.pageWrap) &&
+        !turnMethods._necessPage.call(this, page))
+      turnMethods._removePageFromDOM.call(this, page);
+    
+  },
+
+  // Removes a page from DOM and its internal references
+
+  _removePageFromDOM: function(page) {
+
+    var data = this.data();
+
+    if (data.pages[page]) {
+      var dd = data.pages[page].data();
+
+      flipMethods._moveFoldingPage.call(data.pages[page], false);
+
+      if (dd.f && dd.f.fwrapper)
+        dd.f.fwrapper.remove();
+
+      data.pages[page].removeData();
+      data.pages[page].remove();
+      delete data.pages[page];
+    }
+
+    if (data.pageObjs[page])
+      data.pageObjs[page].remove();
+
+    if (data.pageWrap[page]) {
+      data.pageWrap[page].remove();
+      delete data.pageWrap[page];
+    }
+
+    turnMethods._removeMv.call(this, page);
+
+    delete data.pagePlace[page];
+    delete data.pageZoom[page];
+
+  },
+
+  // Removes a page
+
+  removePage: function(page) {
+
+    var data = this.data();
+
+    // Delete all the pages
+    if (page=='*') {
+      
+      while (data.totalPages!==0) {
+        this.turn('removePage', data.totalPages);
+      }
+
+    } else {
+
+      if (page<1 || page>data.totalPages)
+        throw turnError('The page '+ page + ' doesn\'t exist');
+        
+      if (data.pageObjs[page]) {
+
+        // Stop animations
+        this.turn('stop');
+
+        // Remove `page`
+        turnMethods._removePageFromDOM.call(this, page);
+
+        delete data.pageObjs[page];
+
+      }
+
+      // Move the pages
+      turnMethods._movePages.call(this, page, -1);
+
+      // Resize the size of this flipbook
+      data.totalPages = data.totalPages-1;
+
+      // Check the current view
+
+      if (data.page>data.totalPages) {
+
+       data.page = null;
+       turnMethods._fitPage.call(this, data.totalPages);
+
+      } else {
+
+        turnMethods._makeRange.call(this);
+        this.turn('update');
+
+      }
+    }
+
+    return this;
+  
+  },
+
+  // Moves pages
+
+  _movePages: function(from, change) {
+
+    var page,
+      that = this,
+      data = this.data(),
+      single = data.display=='single',
+      move = function(page) {
+
+        var next = page + change,
+          odd = next%2,
+          className = (odd) ? ' odd ' : ' even ';
+
+        if (data.pageObjs[page])
+          data.pageObjs[next] = data.pageObjs[page].
+            removeClass('p' + page + ' odd even').
+            addClass('p' + next + className);
+
+        if (data.pagePlace[page] && data.pageWrap[page]) {
+
+          data.pagePlace[next] = next;
+        
+          if (data.pageObjs[next].hasClass('fixed'))
+            data.pageWrap[next] = data.pageWrap[page].
+              attr('page', next);
+          else
+            data.pageWrap[next] = data.pageWrap[page].
+              css(turnMethods._pageSize.call(that, next, true)).
+              attr('page', next);
+      
+            if (data.pages[page])
+              data.pages[next] = data.pages[page].
+                flip('options', {
+                  page: next,
+                  next: (single || odd) ? next+1 : next-1
+                });
+
+            if (change) {
+              delete data.pages[page];
+              delete data.pagePlace[page];
+              delete data.pageZoom[page];
+              delete data.pageObjs[page];
+              delete data.pageWrap[page];
+            }
+
+        }
+
+    };
+
+    if (change>0)
+      for (page=data.totalPages; page>=from; page--)
+        move(page);
+    else
+      for (page=from; page<=data.totalPages; page++)
+        move(page);
+
+  },
+
+  // Sets or Gets the display mode
+
+  display: function(display) {
+
+    var data = this.data(),
+      currentDisplay = data.display;
+
+    if (display===undefined) {
+      
+      return currentDisplay;
+
+    } else {
+
+      if ($.inArray(display, displays)==-1)
+        throw turnError('"'+display + '" is not a value for display');
+      
+      switch(display) {
+        case 'single':
+
+          // Create a temporal page to use as folded page
+
+          if (!data.pageObjs[0]) {
+            this.turn('stop').
+              css({'overflow': 'hidden'});
+
+            data.pageObjs[0] = $('<div />',
+                {'class': 'page p-temporal'}).
+              css({width: this.width(), height: this.height()}).
+              appendTo(this);
+          }
+
+          this.addClass('shadow');
+
+        break;
+        case 'double':
+
+          // Remove the temporal page
+
+          if (data.pageObjs[0]) {
+            this.turn('stop').css({'overflow': ''});
+            data.pageObjs[0].remove();
+            delete data.pageObjs[0];
+          }
+
+          this.removeClass('shadow');
+
+        break;
+      }
+      
+
+      data.display = display;
+
+      if (currentDisplay) {
+        var size = this.turn('size');
+        turnMethods._movePages.call(this, 1, 0);
+        this.turn('size', size.width, size.height).
+          turn('update');
+      }
+
+      return this;
+
+    }
+  
+  },
+  
+  // Gets and sets the direction of the flipbook
+
+  direction: function(dir) {
+
+    var data = this.data();
+
+    if (dir===undefined) {
+
+      return data.direction;
+
+    } else {
+
+      dir = dir.toLowerCase();
+
+      if ($.inArray(dir, directions)==-1)
+        throw turnError('"' + dir + '" is not a value for direction');
+
+      if (dir=='rtl') {
+        $(this).attr('dir', 'ltr').
+          css({direction: 'ltr'});
+      }
+
+      data.direction = dir;
+
+      if (data.done)
+        this.turn('size', $(this).width(), $(this).height());
+
+      return this;
+    }
+
+  },
+
+  // Detects animation
+
+  animating: function() {
+
+    return this.data().pageMv.length>0;
+
+  },
+
+  // Gets the current activated corner
+
+  corner: function() {
+    
+    var corner,
+      page,
+      data = this.data();
+
+    for (page in data.pages) {
+      if (has(page, data.pages))
+        if ((corner = data.pages[page].flip('corner'))) {
+          return corner;
+        }
+    }
+
+    return false;
+  },
+
+  // Gets the data stored in the flipbook
+
+  data: function() {
+    
+    return this.data();
+
+  },
+
+  // Disables and enables the effect
+
+  disable: function(disable) {
+
+    var page,
+      data = this.data(),
+      view = this.turn('view');
+
+    data.disabled = disable===undefined || disable===true;
+
+    for (page in data.pages) {
+      if (has(page, data.pages))
+        data.pages[page].flip('disable',
+          (data.disabled) ? true : $.inArray(parseInt(page, 10), view)==-1);
+    }
+
+    return this;
+
+  },
+
+  // Disables and enables the effect
+
+  disabled: function(disable) {
+
+    if (disable===undefined) {
+      return this.data().disabled===true;
+    } else {
+      return this.turn('disable', disable);
+    }
+
+  },
+
+  // Gets and sets the size
+
+  size: function(width, height) {
+
+    if (width===undefined || height===undefined) {
+      
+      return {width: this.width(), height: this.height()};
+
+    } else {
+
+      this.turn('stop');
+
+      var page, prop,
+        data = this.data(),
+        pageWidth = (data.display=='double') ? width/2 : width;
+
+      this.css({width: width, height: height});
+
+      if (data.pageObjs[0])
+        data.pageObjs[0].css({width: pageWidth, height: height});
+      
+      for (page in data.pageWrap) {
+        if (!has(page, data.pageWrap)) continue;
+
+        prop = turnMethods._pageSize.call(this, page, true);
+
+        data.pageObjs[page].css({width: prop.width, height: prop.height});
+        data.pageWrap[page].css(prop);
+        
+        if (data.pages[page])
+          data.pages[page].css({width: prop.width, height: prop.height});
+      }
+
+      this.turn('resize');
+
+      return this;
+
+    }
+  },
+
+  // Resizes each page
+
+  resize: function() {
+
+    var page, data = this.data();
+
+    if (data.pages[0]) {
+      data.pageWrap[0].css({left: -this.width()});
+      data.pages[0].flip('resize', true);
+    }
+
+    for (page = 1; page <= data.totalPages; page++)
+      if (data.pages[page])
+        data.pages[page].flip('resize', true);
+
+    turnMethods._updateShadow.call(this);
+
+    if (data.opts.autoCenter)
+      this.turn('center');
+
+  },
+
+  // Removes an animation from the cache
+
+  _removeMv: function(page) {
+
+    var i, data = this.data();
+      
+    for (i=0; i<data.pageMv.length; i++)
+      if (data.pageMv[i]==page) {
+        data.pageMv.splice(i, 1);
+        return true;
+      }
+
+    return false;
+
+  },
+
+  // Adds an animation to the cache
+  
+  _addMv: function(page) {
+
+    var data = this.data();
+
+    turnMethods._removeMv.call(this, page);
+    data.pageMv.push(page);
+
+  },
+
+  // Gets indexes for a view
+
+  _view: function(page) {
+  
+    var data = this.data();
+    
+    page = page || data.page;
+
+    if (data.display=='double')
+      return (page%2) ? [page-1, page] : [page, page+1];
+    else
+      return [page];
+
+  },
+
+  // Gets a view
+
+  view: function(page) {
+
+    var data = this.data(),
+      view = turnMethods._view.call(this, page);
+
+    if (data.display=='double')
+      return [(view[0]>0) ? view[0] : 0,
+        (view[1]<=data.totalPages) ? view[1] : 0];
+    else
+      return [(view[0]>0 && view[0]<=data.totalPages) ? view[0] : 0];
+
+  },
+
+  // Stops animations
+
+  stop: function(ignore, animate) {
+
+    if (this.turn('animating')) {
+  
+      var i, opts, page,
+        data = this.data();
+
+      if (data.tpage) {
+        data.page = data.tpage;
+        delete data['tpage'];
+      }
+
+      for (i = 0; i<data.pageMv.length; i++) {
+
+        if (!data.pageMv[i] || data.pageMv[i]===ignore)
+          continue;
+
+        page = data.pages[data.pageMv[i]];
+        opts = page.data().f.opts;
+
+        page.flip('hideFoldedPage', animate);
+
+        if (!animate)
+          flipMethods._moveFoldingPage.call(page, false);
+
+        if (opts.force) {
+          opts.next = (opts.page%2===0) ? opts.page-1 : opts.page+1;
+          delete opts['force'];
+        }
+
+      }
+    }
+    
+    this.turn('update');
+
+    return this;
+  },
+
+  // Gets and sets the number of pages
+
+  pages: function(pages) {
+
+    var data = this.data();
+
+    if (pages) {
+
+      if (pages<data.totalPages) {
+
+        for (var page = data.totalPages; page>pages; page--)
+          this.turn('removePage', page);
+
+      }
+
+        data.totalPages = pages;
+        turnMethods._fitPage.call(this, data.page);
+
+      return this;
+
+    } else
+      return data.totalPages;
+
+  },
+
+  // Checks missing pages
+
+  _missing : function(page) {
+    
+    var data = this.data();
+
+    if (data.totalPages<1)
+      return;
+
+    var p,
+        range = this.turn('range', page),
+        missing = [];
+
+    for (p = range[0]; p<=range[1]; p++) {
+      if (!data.pageObjs[p])
+        missing.push(p);
+    }
+
+    if (missing.length>0)
+      this.trigger('missing', [missing]);
+
+  },
+
+  // Sets a page without effect
+
+  _fitPage: function(page) {
+
+    var data = this.data(),
+      newView = this.turn('view', page);
+
+    turnMethods._missing.call(this, page);
+    
+    if (!data.pageObjs[page])
+      return;
+
+    data.page = page;
+  
+    this.turn('stop');
+
+    for (var i = 0; i<newView.length; i++) {
+
+      if (newView[i] && data.pageZoom[newView[i]]!=data.zoom) {
+  
+        this.trigger('zoomed',[
+          newView[i],
+          newView,
+          data.pageZoom[newView[i]],
+          data.zoom]);
+
+        data.pageZoom[newView[i]] = data.zoom;
+
+      }
+    }
+
+    turnMethods._removeFromDOM.call(this);
+    turnMethods._makeRange.call(this);
+    turnMethods._updateShadow.call(this);
+    this.trigger('turned', [page, newView]);
+    this.turn('update');
+
+    if (data.opts.autoCenter)
+      this.turn('center');
+
+  },
+  
+  // Turns the page
+
+  _turnPage: function(page) {
+
+    var current,
+      next,
+      data = this.data(),
+      place = data.pagePlace[page],
+      view = this.turn('view'),
+      newView = this.turn('view', page);
+
+
+    if (data.page!=page) {
+
+      var currentPage = data.page;
+
+      if (trigger('turning', this, [page, newView])=='prevented') {
+
+        if (currentPage==data.page && $.inArray(place, data.pageMv)!=-1)
+          data.pages[place].flip('hideFoldedPage', true);
+        
+        return;
+
+      }
+
+      if ($.inArray(1, newView)!=-1)
+        this.trigger('first');
+      if ($.inArray(data.totalPages, newView)!=-1)
+        this.trigger('last');
+
+    }
+
+    if (data.display=='single') {
+      current = view[0];
+      next = newView[0];
+    } else if (view[1] && page>view[1]) {
+      current = view[1];
+      next = newView[0];
+    } else if (view[0] && page<view[0]) {
+      current = view[0];
+      next = newView[1];
+    }
+
+    var optsCorners = data.opts.turnCorners.split(','),
+      flipData = data.pages[current].data().f,
+      opts = flipData.opts,
+      actualPoint = flipData.point;
+
+    turnMethods._missing.call(this, page);
+    
+    if (!data.pageObjs[page])
+      return;
+
+    this.turn('stop');
+
+    data.page = page;
+
+    turnMethods._makeRange.call(this);
+
+    data.tpage = next;
+
+    if (opts.next!=next) {
+      opts.next = next;
+      opts.force = true;
+    }
+
+    this.turn('update');
+
+    flipData.point = actualPoint;
+    
+    if (flipData.effect=='hard')
+      if (data.direction=='ltr')
+        data.pages[current].flip('turnPage',
+          (page>current) ? 'r' : 'l');
+      else
+        data.pages[current].flip('turnPage',
+          (page>current) ? 'l' : 'r');
+    else {
+      if (data.direction=='ltr')
+        data.pages[current].flip('turnPage',
+          optsCorners[(page>current) ? 1 : 0]);
+      else
+        data.pages[current].flip('turnPage',
+          optsCorners[(page>current) ? 0 : 1]);
+    }
+
+  },
+
+  // Gets and sets a page
+
+  page: function(page) {
+
+    var data = this.data();
+
+    if (page===undefined) {
+      
+      return data.page;
+
+    } else {
+
+      if (!data.disabled && !data.destroying) {
+
+        page = parseInt(page, 10);
+
+        if (page>0 && page<=data.totalPages) {
+
+          if (page!=data.page) {
+            if (!data.done || $.inArray(page, this.turn('view'))!=-1)
+              turnMethods._fitPage.call(this, page);
+            else
+              turnMethods._turnPage.call(this, page);
+          }
+      
+          return this;
+
+        } else {
+        
+          throw turnError('The page ' + page + ' does not exist');
+
+        }
+
+      }
+
+    }
+
+  },
+
+  // Turns to the next view
+
+  next: function() {
+
+    return this.turn('page', Math.min(this.data().totalPages,
+      turnMethods._view.call(this, this.data().page).pop() + 1));
+  
+  },
+
+  // Turns to the previous view
+
+  previous: function() {
+
+    return this.turn('page', Math.max(1,
+      turnMethods._view.call(this, this.data().page).shift() - 1));
+
+  },
+
+  // Shows a peeling corner
+
+  peel: function(corner, animate) {
+    
+    var data = this.data(),
+      view = this.turn('view');
+
+    animate = (animate===undefined) ? true : animate===true;
+
+    if (corner===false) {
+      
+      this.turn('stop', null, animate);
+
+    } else {
+    
+      if (data.display=='single') {
+
+        data.pages[data.page].flip('peel', corner, animate);
+
+      } else {
+
+        var page;
+
+        if (data.direction=='ltr') {
+          
+          page = (corner.indexOf('l')!=-1) ? view[0] : view[1];
+
+        } else {
+          
+          page = (corner.indexOf('l')!=-1) ? view[1] : view[0];
+
+        }
+        
+        if (data.pages[page])
+          data.pages[page].flip('peel', corner, animate);
+
+      }
+    }
+
+    return this;
+
+  },
+
+  // Adds a motion to the internal list
+  // This event is called in context of flip
+
+  _addMotionPage: function() {
+
+    var opts = $(this).data().f.opts,
+      turn = opts.turn,
+      dd = turn.data();
+
+    turnMethods._addMv.call(turn, opts.page);
+
+  },
+
+  // This event is called in context of flip
+
+  _eventStart: function(e, opts, corner) {
+
+    var data = opts.turn.data(),
+      actualZoom = data.pageZoom[opts.page];
+
+    if (e.isDefaultPrevented()) {
+      turnMethods._updateShadow.call(opts.turn);
+      return;
+    }
+
+    if (actualZoom && actualZoom!=data.zoom) {
+      
+      opts.turn.trigger('zoomed',[
+        opts.page,
+        opts.turn.turn('view', opts.page),
+        actualZoom,
+        data.zoom]);
+
+      data.pageZoom[opts.page] = data.zoom;
+
+    }
+
+    if (data.display=='single' && corner) {
+
+      if ((corner.charAt(1)=='l' && data.direction=='ltr') ||
+        (corner.charAt(1)=='r' && data.direction=='rtl'))
+      {
+        
+        opts.next = (opts.next<opts.page) ? opts.next : opts.page-1;
+        opts.force = true;
+
+      } else {
+        
+        opts.next = (opts.next>opts.page) ? opts.next : opts.page+1;
+
+      }
+
+    }
+
+    turnMethods._addMotionPage.call(e.target);
+    turnMethods._updateShadow.call(opts.turn);
+  },
+
+  // This event is called in context of flip
+
+  _eventEnd: function(e, opts, turned) {
+  
+    var that = $(e.target),
+      data = that.data().f,
+      turn = opts.turn,
+      dd = turn.data();
+
+    if (turned) {
+
+      var tpage = dd.tpage || dd.page;
+    
+      if (tpage==opts.next || tpage==opts.page) {
+        delete dd.tpage;
+
+        turnMethods._fitPage.call(turn, tpage || opts.next, true);
+      }
+
+    } else {
+      
+      turnMethods._removeMv.call(turn, opts.page);
+      turnMethods._updateShadow.call(turn);
+      turn.turn('update');
+
+    }
+    
+  },
+  
+  // This event is called in context of flip
+
+  _eventPressed: function(e) {
+
+    var page,
+      data = $(e.target).data().f,
+      turn = data.opts.turn,
+      turnData = turn.data(),
+      pages = turnData.pages;
+    
+    turnData.mouseAction = true;
+
+    turn.turn('update');
+
+    return data.time = new Date().getTime();
+
+  },
+
+  // This event is called in context of flip
+
+  _eventReleased: function(e, point) {
+
+    var outArea,
+      page = $(e.target),
+      data = page.data().f,
+      turn = data.opts.turn,
+      turnData = turn.data();
+    
+    if (turnData.display=='single') {
+      outArea = (point.corner=='br' || point.corner=='tr') ?
+        point.x<page.width()/2:
+        point.x>page.width()/2;
+    } else {
+      outArea = point.x<0 || point.x>page.width();
+    }
+
+    if ((new Date()).getTime()-data.time<200 || outArea) {
+
+      e.preventDefault();
+      turnMethods._turnPage.call(turn, data.opts.next);
+
+    }
+
+    turnData.mouseAction = false;
+
+  },
+
+  // This event is called in context of flip
+  
+  _flip: function(e) {
+
+    e.stopPropagation();
+
+    var opts = $(e.target).data().f.opts;
+
+    opts.turn.trigger('turn', [opts.next]);
+
+    if (opts.turn.data().opts.autoCenter) {
+      opts.turn.turn('center', opts.next);
+    }
+
+  },
+
+ //
+  _touchStart: function() {
+    var data = this.data();
+    for (var page in data.pages) {
+      if (has(page, data.pages) &&
+        flipMethods._eventStart.apply(data.pages[page], arguments)===false) {
+          return false;
+      }
+    }
+  },
+  
+  //
+  _touchMove: function() {
+    var data = this.data();
+    for (var page in data.pages) {
+      if (has(page, data.pages)) {
+        flipMethods._eventMove.apply(data.pages[page], arguments);
+      }
+    }
+  },
+
+  //
+  _touchEnd: function() {
+    var data = this.data();
+    for (var page in data.pages) {
+      if (has(page, data.pages)) {
+        flipMethods._eventEnd.apply(data.pages[page], arguments);
+      }
+    }
+  },
+
+  // Calculate the z-index value for pages during the animation
+
+  calculateZ: function(mv) {
+
+    var i, page, nextPage, placePage, dpage,
+      that = this,
+      data = this.data(),
+      view = this.turn('view'),
+      currentPage = view[0] || view[1],
+      total = mv.length-1,
+      r = {pageZ: {}, partZ: {}, pageV: {}},
+
+      addView = function(page) {
+        var view = that.turn('view', page);
+        if (view[0]) r.pageV[view[0]] = true;
+        if (view[1]) r.pageV[view[1]] = true;
+      };
+    
+    for (i = 0; i<=total; i++) {
+      page = mv[i];
+      nextPage = data.pages[page].data().f.opts.next;
+      placePage = data.pagePlace[page];
+      addView(page);
+      addView(nextPage);
+      dpage = (data.pagePlace[nextPage]==nextPage) ? nextPage : page;
+      r.pageZ[dpage] = data.totalPages - Math.abs(currentPage-dpage);
+      r.partZ[placePage] = data.totalPages*2 -  total + i;
+    }
+
+    return r;
+  },
+
+  // Updates the z-index and display property of every page
+
+  update: function() {
+
+    var page,
+      data = this.data();
+
+    if (this.turn('animating') && data.pageMv[0]!==0) {
+
+      // Update motion
+
+      var p, apage, fixed,
+        pos = this.turn('calculateZ', data.pageMv),
+        corner = this.turn('corner'),
+        actualView = this.turn('view'),
+        newView = this.turn('view', data.tpage);
+  
+      for (page in data.pageWrap) {
+
+        if (!has(page, data.pageWrap))
+          continue;
+
+        fixed = data.pageObjs[page].hasClass('fixed');
+
+        data.pageWrap[page].css({
+          display: (pos.pageV[page] || fixed) ? '' : 'none',
+          zIndex:
+            (data.pageObjs[page].hasClass('hard') ?
+              pos.partZ[page]
+              :
+              pos.pageZ[page]
+            ) || (fixed ? -1 : 0)
+        });
+
+        if ((p = data.pages[page])) {
+
+          p.flip('z', pos.partZ[page] || null);
+
+          if (pos.pageV[page])
+            p.flip('resize');
+          
+          if (data.tpage) { // Is it turning the page to `tpage`?
+
+            p.flip('hover', false).
+              flip('disable',
+                $.inArray(parseInt(page, 10), data.pageMv)==-1 &&
+                page!=newView[0] &&
+                page!=newView[1]);
+
+          } else {
+
+            p.flip('hover', corner===false).
+              flip('disable', page!=actualView[0] && page!=actualView[1]);
+
+          }
+
+        }
+
+      }
+
+    } else {
+
+      // Update static pages
+
+      for (page in data.pageWrap) {
+
+        if (!has(page, data.pageWrap))
+          continue;
+
+        var pageLocation = turnMethods._setPageLoc.call(this, page);
+
+        if (data.pages[page]) {
+          data.pages[page].
+            flip('disable', data.disabled || pageLocation!=1).
+            flip('hover', true).
+            flip('z', null);
+        }
+      }
+    }
+
+    return this;
+  },
+
+  // Updates the position and size of the flipbook's shadow
+
+  _updateShadow: function() {
+    
+    var view, view2, shadow,
+      data = this.data(),
+      width = this.width(),
+      height = this.height(),
+      pageWidth = (data.display=='single') ? width : width/2;
+
+    view = this.turn('view');
+
+    if (!data.shadow) {
+      data.shadow = $('<div />', {
+          'class': 'shadow',
+          'css': divAtt(0, 0, 0).css
+        }).
+        appendTo(this);
+    }
+
+    for (var i = 0; i<data.pageMv.length; i++) {
+      if (!view[0] || !view[1])
+        break;
+    
+      view = this.turn('view', data.pages[data.pageMv[i]].data().f.opts.next);
+      view2 = this.turn('view', data.pageMv[i]);
+
+      view[0] = view[0] && view2[0];
+      view[1] = view[1] && view2[1];
+    }
+
+    if (!view[0]) shadow = (data.direction=='ltr') ? 1 : 2;
+    else if (!view[1]) shadow = (data.direction=='ltr') ? 2 : 1;
+    else shadow = 3;
+
+    switch (shadow) {
+      case 1:
+        data.shadow.css({
+          width: pageWidth,
+          height: height,
+          top: 0,
+          left: pageWidth
+        });
+        break;
+      case 2:
+        data.shadow.css({
+          width: pageWidth,
+          height: height,
+          top: 0,
+          left: 0
+        });
+        break;
+      case 3:
+        data.shadow.css({
+          width: width,
+          height: height,
+          top: 0,
+          left: 0
+        });
+        break;
+    }
+
+  },
+
+  // Sets the z-index and display property of a page
+  // It depends on the current view
+
+  _setPageLoc: function(page) {
+
+    var data = this.data(),
+      view = this.turn('view'),
+      loc = 0;
+
+    
+    if (page==view[0] || page==view[1])
+      loc = 1;
+    else if (
+      (data.display=='single' && page==view[0]+1) ||
+      (data.display=='double' && page==view[0]-2 || page==view[1]+2)
+    )
+      loc = 2;
+
+    if (!this.turn('animating'))
+      switch (loc) {
+        case 1:
+          data.pageWrap[page].css(
+          {
+            zIndex: data.totalPages,
+            display: ''
+          });
+        break;
+        case 2:
+          data.pageWrap[page].css(
+          {
+            zIndex: data.totalPages-1,
+            display: ''
+          });
+        break;
+        case 0:
+          data.pageWrap[page].css(
+          {
+            zIndex: 0,
+            display: (data.pageObjs[page].hasClass('fixed')) ? '' : 'none'}
+          );
+        break;
+      }
+    
+    return loc;
+  },
+
+  // Gets and sets the options
+
+  options: function(options) {
+    
+    if (options===undefined) {
+      
+      return this.data().opts;
+
+    } else {
+
+      var data = this.data();
+
+      // Set new values
+
+      $.extend(data.opts, options);
+      
+      // Set pages
+
+      if (options.pages)
+        this.turn('pages', options.pages);
+
+      // Set page
+
+      if (options.page)
+        this.turn('page', options.page);
+
+      // Set display
+
+      if (options.display)
+        this.turn('display', options.display);
+      
+      // Set direction
+
+      if (options.direction)
+        this.turn('direction', options.direction);
+
+      // Set size
+
+      if (options.width && options.height)
+        this.turn('size', options.width, options.height);
+      
+      // Add event listeners
+
+      if (options.when)
+        for (var eventName in options.when)
+          if (has(eventName, options.when)) {
+            this.unbind(eventName).
+              bind(eventName, options.when[eventName]);
+          }
+
+      return this;
+    }
+
+  },
+
+  // Gets the current version
+
+  version: function() {
+
+    return version;
+
+  }
+},
+
+// Methods and properties for the flip page effect
+
+flipMethods = {
+
+  // Constructor
+
+  init: function(opts) {
+
+    this.data({f: {
+      disabled: false,
+      hover: false,
+      effect: (this.hasClass('hard')) ? 'hard' : 'sheet'
+    }});
+  
+    this.flip('options', opts);
+
+    flipMethods._addPageWrapper.call(this);
+
+    return this;
+  },
+
+  setData: function(d) {
+    
+    var data = this.data();
+
+    data.f = $.extend(data.f, d);
+
+    return this;
+  },
+
+  options: function(opts) {
+    
+    var data = this.data().f;
+
+    if (opts) {
+      flipMethods.setData.call(this,
+        {opts: $.extend({}, data.opts || flipOptions, opts)});
+      return this;
+    } else
+      return data.opts;
+
+  },
+
+  z: function(z) {
+    
+    var data = this.data().f;
+
+    data.opts['z-index'] = z;
+
+    if (data.fwrapper)
+      data.fwrapper.css({
+        zIndex: z || parseInt(data.parent.css('z-index'), 10) || 0
+      });
+
+    return this;
+  },
+
+  _cAllowed: function() {
+    
+    var data = this.data().f,
+      page = data.opts.page,
+      turnData = data.opts.turn.data(),
+      odd = page%2;
+    
+    if (data.effect=='hard') {
+    
+      return (turnData.direction=='ltr') ?
+        [(odd) ? 'r' : 'l'] :
+        [(odd) ? 'l' : 'r'];
+
+    } else {
+
+      if (turnData.display=='single') {
+
+        if (page==1)
+          return (turnData.direction=='ltr') ?
+            corners['forward'] : corners['backward'];
+        else if (page==turnData.totalPages)
+          return (turnData.direction=='ltr') ?
+            corners['backward'] : corners['forward'];
+        else
+          return corners['all'];
+
+      } else {
+
+        return (turnData.direction=='ltr') ?
+          corners[(odd) ? 'forward' : 'backward']
+          :
+          corners[(odd) ? 'backward' : 'forward'];
+
+      }
+
+    }
+
+  },
+
+  _cornerActivated: function(p) {
+
+    var data = this.data().f,
+      width = this.width(),
+      height = this.height(),
+      point = {x: p.x, y: p.y, corner: ''},
+      csz = data.opts.cornerSize;
+
+    if (point.x<=0 || point.y<=0 || point.x>=width || point.y>=height)
+      return false;
+
+    var allowedCorners = flipMethods._cAllowed.call(this);
+
+    switch (data.effect) {
+      case 'hard':
+        
+        if (point.x>width-csz)
+          point.corner = 'r';
+        else if (point.x<csz)
+          point.corner = 'l';
+        else
+          return false;
+            
+        break;
+
+      case 'sheet':
+        
+        if (point.y<csz)
+          point.corner+= 't';
+        else if (point.y>=height-csz)
+          point.corner+= 'b';
+        else
+          return false;
+    
+        if (point.x<=csz)
+          point.corner+= 'l';
+        else if (point.x>=width-csz)
+          point.corner+= 'r';
+        else
+          return false;
+     
+        break;
+      }
+
+    return (!point.corner || $.inArray(point.corner, allowedCorners)==-1) ?
+      false : point;
+
+  },
+
+  _isIArea: function(e) {
+
+    var pos = this.data().f.parent.offset();
+
+    e = (isTouch && e.originalEvent) ? e.originalEvent.touches[0] : e;
+
+    return flipMethods._cornerActivated.call(this,
+      {
+        x: e.pageX-pos.left,
+        y: e.pageY-pos.top
+      });
+
+  },
+
+  _c: function(corner, opts) {
+
+    opts = opts || 0;
+
+    switch (corner) {
+      case 'tl':
+        return point2D(opts, opts);
+      case 'tr':
+        return point2D(this.width()-opts, opts);
+      case 'bl':
+        return point2D(opts, this.height()-opts);
+      case 'br':
+        return point2D(this.width()-opts, this.height()-opts);
+      case 'l':
+        return point2D(opts, 0);
+      case 'r':
+        return point2D(this.width()-opts, 0);
+    }
+
+  },
+
+  _c2: function(corner) {
+
+    switch (corner) {
+      case 'tl':
+        return point2D(this.width()*2, 0);
+      case 'tr':
+        return point2D(-this.width(), 0);
+      case 'bl':
+        return point2D(this.width()*2, this.height());
+      case 'br':
+        return point2D(-this.width(), this.height());
+      case 'l':
+        return point2D(this.width()*2, 0);
+      case 'r':
+        return point2D(-this.width(), 0);
+    }
+
+  },
+
+  _foldingPage: function() {
+
+    var data = this.data().f;
+
+    if (!data)
+      return;
+
+    var opts = data.opts;
+
+    if (opts.turn) {
+      data = opts.turn.data();
+      if (data.display == 'single')
+        return (opts.next>1 || opts.page>1) ? data.pageObjs[0] : null;
+      else
+        return data.pageObjs[opts.next];
+    }
+
+  },
+
+  _backGradient: function() {
+
+    var data = this.data().f,
+      turnData = data.opts.turn.data(),
+      gradient =  turnData.opts.gradients && (turnData.display=='single' ||
+         (data.opts.page!=2 && data.opts.page!=turnData.totalPages-1));
+
+    if (gradient && !data.bshadow)
+      data.bshadow = $('<div/>', divAtt(0, 0, 1)).
+        css({'position': '', width: this.width(), height: this.height()}).
+        appendTo(data.parent);
+
+    return gradient;
+
+  },
+
+  type: function () {
+    
+    return this.data().f.effect;
+
+  },
+
+  resize: function(full) {
+    
+    var data = this.data().f,
+      turnData = data.opts.turn.data(),
+      width = this.width(),
+      height = this.height();
+
+    switch (data.effect) {
+      case 'hard':
+        
+      if (full) {
+        data.wrapper.css({width: width, height: height});
+        data.fpage.css({width: width, height: height});
+        if (turnData.opts.gradients) {
+          data.ashadow.css({width: width, height: height});
+          data.bshadow.css({width: width, height: height});
+        }
+      }
+
+      break;
+      case 'sheet':
+
+      if (full) {
+        var size = Math.round(Math.sqrt(Math.pow(width, 2)+Math.pow(height, 2)));
+
+        data.wrapper.css({width: size, height: size});
+        data.fwrapper.css({width: size, height: size}).
+          children(':first-child').
+          css({width: width, height: height});
+
+        data.fpage.css({width: width, height: height});
+
+        if (turnData.opts.gradients)
+          data.ashadow.css({width: width, height: height});
+
+        if (flipMethods._backGradient.call(this))
+          data.bshadow.css({width: width, height: height});
+      }
+
+      if (data.parent.is(':visible')) {
+        var offset = findPos(data.parent[0]);
+
+        data.fwrapper.css({top: offset.top,
+          left: offset.left});
+
+        //if (data.opts.turn) {
+         offset = findPos(data.opts.turn[0]);
+         data.fparent.css({top: -offset.top, left: -offset.left});
+        //}
+      }
+
+      this.flip('z', data.opts['z-index']);
+
+      break;
+    }
+
+  },
+
+  // Prepares the page by adding a general wrapper and another objects
+
+  _addPageWrapper: function() {
+
+    var att,
+      data = this.data().f,
+      turnData = data.opts.turn.data(),
+      parent = this.parent();
+
+    data.parent = parent;
+
+  if (!data.wrapper)
+    switch (data.effect) {
+      case 'hard':
+        
+        var cssProperties = {};
+        cssProperties[vendor + 'transform-style'] = 'preserve-3d';
+        cssProperties[vendor + 'backface-visibility'] = 'hidden';
+
+        data.wrapper = $('<div/>', divAtt(0, 0, 2)).
+          css(cssProperties).
+          appendTo(parent).
+          prepend(this);
+
+        data.fpage = $('<div/>', divAtt(0, 0, 1)).
+          css(cssProperties).
+          appendTo(parent);
+        
+        if (turnData.opts.gradients) {
+          data.ashadow = $('<div/>', divAtt(0, 0,  0)).
+            hide().
+            appendTo(parent);
+
+          data.bshadow = $('<div/>', divAtt(0, 0,  0));
+        }
+
+      break;
+      case 'sheet':
+        
+        var width = this.width(),
+          height = this.height(),
+          size = Math.round(Math.sqrt(Math.pow(width, 2)+Math.pow(height, 2)));
+        
+        data.fparent = data.opts.turn.data().fparent;
+
+        if (!data.fparent) {
+          var fparent = $('<div/>', {css: {'pointer-events': 'none'}}).hide();
+            fparent.data().flips = 0;
+            fparent.css(divAtt(0, 0, 'auto', 'visible').css).
+            appendTo(data.opts.turn);
+            
+            data.opts.turn.data().fparent = fparent;
+            data.fparent = fparent;
+        }
+
+        this.css({position: 'absolute', top: 0, left: 0, bottom: 'auto', right: 'auto'});
+
+        data.wrapper = $('<div/>', divAtt(0, 0, this.css('z-index'))).
+          appendTo(parent).
+          prepend(this);
+
+        data.fwrapper = $('<div/>', divAtt(parent.offset().top, parent.offset().left)).
+          hide().
+          appendTo(data.fparent);
+
+        data.fpage = $('<div/>', divAtt(0, 0, 0, 'visible')).
+          css({cursor: 'default'}).
+          appendTo(data.fwrapper);
+
+        if (turnData.opts.gradients)
+          data.ashadow = $('<div/>', divAtt(0, 0,  1)).
+          appendTo(data.fpage);
+
+        flipMethods.setData.call(this, data);
+
+      break;
+    }
+
+    // Set size
+    flipMethods.resize.call(this, true);
+
+  },
+
+  // Takes a 2P point from the screen and applies the transformation
+
+  _fold: function(point) {
+
+    var data = this.data().f,
+      turnData = data.opts.turn.data(),
+      o = flipMethods._c.call(this, point.corner),
+      width = this.width(),
+      height = this.height();
+
+    switch (data.effect) {
+
+      case 'hard':
+
+        if (point.corner=='l')
+          point.x = Math.min(Math.max(point.x, 0), width*2);
+        else
+          point.x = Math.max(Math.min(point.x, width), -width);
+
+        var leftPos,
+          shadow,
+          gradientX,
+          fpageOrigin,
+          parentOrigin,
+          totalPages = turnData.totalPages,
+          zIndex = data.opts['z-index'] || totalPages,
+          parentCss = {'overflow': 'visible'},
+          relX = (o.x) ? (o.x - point.x)/width : point.x/width,
+          angle = relX * 90,
+          half = angle<90;
+
+        switch (point.corner) {
+          case 'l':
+
+            fpageOrigin =  '0% 50%';
+            parentOrigin =  '100% 50%';
+
+            if (half) {
+              leftPos = 0;
+              shadow = data.opts.next-1>0;
+              gradientX = 1;
+            } else {
+              leftPos = '100%';
+              shadow = data.opts.page+1<totalPages;
+              gradientX = 0;
+            }
+
+          break;
+          case 'r':
+
+            fpageOrigin =  '100% 50%';
+            parentOrigin =  '0% 50%';
+            angle = -angle;
+            width = -width;
+
+            if (half) {
+              leftPos = 0;
+              shadow = data.opts.next+1<totalPages;
+              gradientX = 0;
+            } else {
+              leftPos = '-100%';
+              shadow = data.opts.page!=1;
+              gradientX = 1;
+            }
+
+          break;
+        }
+
+        parentCss[vendor+'perspective-origin'] = parentOrigin;
+
+        data.wrapper.transform('rotateY('+angle+'deg)' +
+          'translate3d(0px, 0px, '+(this.attr('depth')||0)+'px)', parentOrigin);
+
+        data.fpage.transform('translateX('+width+'px) rotateY('+(180+angle)+'deg)', fpageOrigin);
+
+        data.parent.css(parentCss);
+
+        if (half) {
+          relX = -relX+1;
+          data.wrapper.css({zIndex: zIndex+1});
+          data.fpage.css({zIndex: zIndex});
+        } else {
+          relX = relX-1;
+          data.wrapper.css({zIndex: zIndex});
+          data.fpage.css({zIndex: zIndex+1});
+        }
+
+        if (turnData.opts.gradients) {
+          if (shadow)
+            data.ashadow.css({
+              display: '',
+              left: leftPos,
+              backgroundColor: 'rgba(0,0,0,'+(0.5*relX)+')'
+            }).
+            transform('rotateY(0deg)');
+          else
+            data.ashadow.hide();
+
+          data.bshadow.css({opacity:-relX + 1});
+
+          if (half) {
+            if (data.bshadow.parent()[0]!=data.wrapper[0]) {
+              data.bshadow.appendTo(data.wrapper);
+            }
+          } else {
+            if (data.bshadow.parent()[0]!=data.fpage[0]) {
+              data.bshadow.appendTo(data.fpage);
+            }
+          }
+          /*data.bshadow.css({
+            backgroundColor: 'rgba(0,0,0,'+(0.1)+')'
+          })*/
+          gradient(data.bshadow, point2D(gradientX * 100, 0), point2D((-gradientX + 1)*100, 0),
+            [[0, 'rgba(0,0,0,0.3)'],[1, 'rgba(0,0,0,0)']],2);
+          
+        }
+
+        break;
+      case 'sheet':
+
+        var that = this,
+          a = 0,
+          alpha = 0,
+          beta,
+          px,
+          gradientEndPointA,
+          gradientEndPointB,
+          gradientStartVal,
+          gradientSize,
+          gradientOpacity,
+          shadowVal,
+          mv = point2D(0, 0),
+          df = point2D(0, 0),
+          tr = point2D(0, 0),
+          folding = flipMethods._foldingPage.call(this),
+          tan = Math.tan(alpha),
+          ac = turnData.opts.acceleration,
+          h = data.wrapper.height(),
+          top = point.corner.substr(0, 1) == 't',
+          left = point.corner.substr(1, 1) == 'l',
+
+      compute = function() {
+
+        var rel = point2D(0, 0);
+        var middle = point2D(0, 0);
+
+        rel.x = (o.x) ? o.x - point.x : point.x;
+
+        if (!hasRot) {
+          rel.y = 0;
+        } else {
+          rel.y = (o.y) ? o.y - point.y : point.y;
+        }
+
+        middle.x = (left)? width - rel.x/2 : point.x + rel.x/2;
+        middle.y = rel.y/2;
+        
+        var alpha =  A90-Math.atan2(rel.y, rel.x),
+          gamma = alpha - Math.atan2(middle.y, middle.x),
+          distance =  Math.max(0, Math.sin(gamma) * Math.sqrt(Math.pow(middle.x, 2) + Math.pow(middle.y, 2)));
+          
+          a = deg(alpha);
+
+          tr = point2D(distance * Math.sin(alpha), distance * Math.cos(alpha));
+
+          if (alpha > A90) {
+            tr.x = tr.x + Math.abs(tr.y * rel.y/rel.x);
+            tr.y = 0;
+            if (Math.round(tr.x*Math.tan(PI-alpha)) < height) {
+              point.y = Math.sqrt(Math.pow(height, 2)+2 * middle.x * rel.x);
+              if (top) point.y =  height - point.y;
+              return compute();
+            }
+          }
+      
+          if (alpha>A90) {
+            var beta = PI-alpha, dd = h - height/Math.sin(beta);
+            mv = point2D(Math.round(dd*Math.cos(beta)), Math.round(dd*Math.sin(beta)));
+            if (left) mv.x = - mv.x;
+            if (top) mv.y = - mv.y;
+          }
+
+          px = Math.round(tr.y/Math.tan(alpha) + tr.x);
+      
+          var side = width - px,
+            sideX = side*Math.cos(alpha*2),
+            sideY = side*Math.sin(alpha*2);
+            df = point2D(
+              Math.round((left ? side -sideX : px+sideX)),
+              Math.round((top) ? sideY : height - sideY));
+          
+        // Gradients
+          if (turnData.opts.gradients) {
+
+            gradientSize = side*Math.sin(alpha);
+
+            var endingPoint = flipMethods._c2.call(that, point.corner),
+             far = Math.sqrt(Math.pow(endingPoint.x-point.x, 2)+Math.pow(endingPoint.y-point.y, 2))/width;
+
+            shadowVal = Math.sin(A90*((far>1) ? 2 - far : far));
+
+            gradientOpacity = Math.min(far, 1);
+
+           
+              gradientStartVal = gradientSize>100 ? (gradientSize-100)/gradientSize : 0;
+
+              gradientEndPointA = point2D(
+                gradientSize*Math.sin(alpha)/width*100,
+                gradientSize*Math.cos(alpha)/height*100);
+           
+
+              if (flipMethods._backGradient.call(that)) {
+
+                gradientEndPointB = point2D(
+                  gradientSize*1.2*Math.sin(alpha)/width*100,
+                  gradientSize*1.2*Math.cos(alpha)/height*100);
+
+                if (!left) gradientEndPointB.x = 100-gradientEndPointB.x;
+                if (!top) gradientEndPointB.y = 100-gradientEndPointB.y;
+
+              }
+
+          }
+
+          tr.x = Math.round(tr.x);
+          tr.y = Math.round(tr.y);
+
+        return true;
+      },
+
+      transform = function(tr, c, x, a) {
+      
+        var f = ['0', 'auto'], mvW = (width-h)*x[0]/100, mvH = (height-h)*x[1]/100,
+          cssA = {left: f[c[0]], top: f[c[1]], right: f[c[2]], bottom: f[c[3]]},
+          cssB = {},
+          aliasingFk = (a!=90 && a!=-90) ? (left ? -1 : 1) : 0,
+          origin = x[0] + '% ' + x[1] + '%';
+
+        that.css(cssA).
+          transform(rotate(a) + translate(tr.x + aliasingFk, tr.y, ac), origin);
+
+        data.fpage.css(cssA).transform(
+          rotate(a) +
+          translate(tr.x + df.x - mv.x - width*x[0]/100, tr.y + df.y - mv.y - height*x[1]/100, ac) +
+          rotate((180/a - 2)*a),
+          origin);
+
+        data.wrapper.transform(translate(-tr.x + mvW-aliasingFk, -tr.y + mvH, ac) + rotate(-a), origin);
+
+        data.fwrapper.transform(translate(-tr.x + mv.x + mvW, -tr.y + mv.y + mvH, ac) + rotate(-a), origin);
+      
+        if (turnData.opts.gradients) {
+
+          if (x[0])
+            gradientEndPointA.x = 100-gradientEndPointA.x;
+
+          if (x[1])
+            gradientEndPointA.y = (100-gradientEndPointA.y);
+
+          cssB['box-shadow'] = '0 0 20px rgba(0,0,0,'+(0.5*shadowVal)+')';
+          folding.css(cssB);
+
+          gradient(data.ashadow,
+              point2D(left?100:0, top?0:100),
+              point2D(gradientEndPointA.x, gradientEndPointA.y),
+              [[gradientStartVal, 'rgba(0,0,0,0)'],
+              [((1-gradientStartVal)*0.8)+gradientStartVal, 'rgba(0,0,0,'+(0.2*gradientOpacity)+')'],
+              [1, 'rgba(255,255,255,'+(0.2*gradientOpacity)+')']],
+              3,
+              alpha);
+
+          if (flipMethods._backGradient.call(that))
+            gradient(data.bshadow,
+                point2D(left?0:100, top?0:100),
+                point2D(gradientEndPointB.x, gradientEndPointB.y),
+                [[0.6, 'rgba(0,0,0,0)'],
+                [0.8, 'rgba(0,0,0,'+(0.3*gradientOpacity)+')'],
+                [1, 'rgba(0,0,0,0)']
+                ],
+                3);
+        }
+
+      };
+
+      switch (point.corner) {
+        case 'l' :
+
+
+        break;
+        case 'r' :
+
+
+        break;
+        case 'tl' :
+          point.x = Math.max(point.x, 1);
+          compute();
+          transform(tr, [1,0,0,1], [100, 0], a);
+        break;
+        case 'tr' :
+          point.x = Math.min(point.x, width-1);
+          compute();
+          transform(point2D(-tr.x, tr.y), [0,0,0,1], [0, 0], -a);
+        break;
+        case 'bl' :
+          point.x = Math.max(point.x, 1);
+          compute();
+          transform(point2D(tr.x, -tr.y), [1,1,0,0], [100, 100], -a);
+        break;
+        case 'br' :
+          point.x = Math.min(point.x, width-1);
+          compute();
+          transform(point2D(-tr.x, -tr.y), [0,1,1,0], [0, 100], a);
+        break;
+      }
+
+    break;
+  }
+
+    data.point = point;
+  
+  },
+
+  _moveFoldingPage: function(move) {
+
+    var data = this.data().f;
+
+    if (!data)
+      return;
+
+    var turn = data.opts.turn,
+      turnData = turn.data(),
+      place = turnData.pagePlace;
+      
+    if (move) {
+
+      var nextPage = data.opts.next;
+  
+      if (place[nextPage]!=data.opts.page) {
+
+        if (data.folding)
+          flipMethods._moveFoldingPage.call(this, false);
+
+        var folding = flipMethods._foldingPage.call(this);
+        
+        folding.appendTo(data.fpage);
+        place[nextPage] = data.opts.page;
+        data.folding = nextPage;
+      }
+
+      turn.turn('update');
+
+    } else {
+
+      if (data.folding) {
+
+        if (turnData.pages[data.folding]) {
+         
+          // If we have flip available
+
+          var flipData = turnData.pages[data.folding].data().f;
+          
+          turnData.pageObjs[data.folding].
+            appendTo(flipData.wrapper);
+
+        } else if (turnData.pageWrap[data.folding]) {
+          
+          // If we have the pageWrapper
+
+          turnData.pageObjs[data.folding].
+            appendTo(turnData.pageWrap[data.folding]);
+
+        }
+
+        if (data.folding in place) {
+          place[data.folding] = data.folding;
+        }
+
+        delete data.folding;
+
+      }
+    }
+  },
+
+  _showFoldedPage: function(c, animate) {
+
+    var folding = flipMethods._foldingPage.call(this),
+      dd = this.data(),
+      data = dd.f,
+      visible = data.visible;
+
+    if (folding) {
+
+      if (!visible || !data.point || data.point.corner!=c.corner) {
+
+        var corner = (
+          data.status=='hover' ||
+          data.status=='peel' ||
+          data.opts.turn.data().mouseAction) ?
+        c.corner : null;
+
+        visible = false;
+
+        if (trigger('start', this, [data.opts, corner])=='prevented')
+          return false;
+
+      }
+
+      if (animate) {
+        
+        var that = this,
+          point = (data.point && data.point.corner==c.corner) ?
+          data.point : flipMethods._c.call(this, c.corner, 1);
+      
+        this.animatef({
+          from: [point.x, point.y],
+          to: [c.x, c.y],
+          duration: 500,
+          frame: function(v) {
+            c.x = Math.round(v[0]);
+            c.y = Math.round(v[1]);
+            flipMethods._fold.call(that, c);
+          }
+        });
+
+      } else  {
+
+        flipMethods._fold.call(this, c);
+
+        if (dd.effect && !dd.effect.turning)
+          this.animatef(false);
+
+      }
+
+      if (!visible) {
+
+        switch(data.effect) {
+          case 'hard':
+
+            data.visible = true;
+            flipMethods._moveFoldingPage.call(this, true);
+            data.fpage.show();
+            if (data.opts.shadows)
+              data.bshadow.show();
+
+          break;
+          case 'sheet':
+
+            data.visible = true;
+            data.fparent.show().data().flips++;
+            flipMethods._moveFoldingPage.call(this, true);
+            data.fwrapper.show();
+            if (data.bshadow)
+              data.bshadow.show();
+
+          break;
+        }
+
+      }
+
+      return true;
+
+    }
+
+    return false;
+  },
+
+  hide: function() {
+
+    var data = this.data().f,
+      turnData = data.opts.turn.data(),
+      folding = flipMethods._foldingPage.call(this);
+
+    switch (data.effect) {
+      case 'hard':
+        
+        if (turnData.opts.gradients) {
+          data.bshadowLoc = 0;
+          data.bshadow.remove();
+          data.ashadow.hide();
+        }
+
+        data.wrapper.transform('');
+        data.fpage.hide();
+
+      break;
+      case 'sheet':
+
+        if ((--data.fparent.data().flips)===0)
+          data.fparent.hide();
+
+        this.css({left: 0, top: 0, right: 'auto', bottom: 'auto'}).
+          transform('');
+
+        data.wrapper.transform('');
+
+        data.fwrapper.hide();
+
+        if (data.bshadow)
+          data.bshadow.hide();
+
+        folding.transform('');
+
+      break;
+    }
+
+      data.visible = false;
+
+    return this;
+  },
+
+  hideFoldedPage: function(animate) {
+
+    var data = this.data().f;
+
+    if (!data.point) return;
+
+    var that = this,
+      p1 = data.point,
+      hide = function() {
+        data.point = null;
+        data.status = '';
+        that.flip('hide');
+        that.trigger('end', [data.opts, false]);
+      };
+
+    if (animate) {
+
+      var p4 = flipMethods._c.call(this, p1.corner),
+        top = (p1.corner.substr(0,1)=='t'),
+        delta = (top) ? Math.min(0, p1.y-p4.y)/2 : Math.max(0, p1.y-p4.y)/2,
+        p2 = point2D(p1.x, p1.y+delta),
+        p3 = point2D(p4.x, p4.y-delta);
+    
+      this.animatef({
+        from: 0,
+        to: 1,
+        frame: function(v) {
+          var np = bezier(p1, p2, p3, p4, v);
+          p1.x = np.x;
+          p1.y = np.y;
+          flipMethods._fold.call(that, p1);
+        },
+        complete: hide,
+        duration: 800,
+        hiding: true
+      });
+
+    } else {
+
+      this.animatef(false);
+      hide();
+
+    }
+  },
+
+  turnPage: function(corner) {
+
+    var that = this,
+      data = this.data().f,
+      turnData = data.opts.turn.data();
+
+    corner = {corner: (data.corner) ?
+      data.corner.corner :
+      corner || flipMethods._cAllowed.call(this)[0]};
+
+    var p1 = data.point ||
+      flipMethods._c.call(this,
+        corner.corner,
+        (data.opts.turn) ? turnData.opts.elevation : 0),
+      p4 = flipMethods._c2.call(this, corner.corner);
+
+      this.trigger('flip').
+        animatef({
+          from: 0,
+          to: 1,
+          frame: function(v) {
+
+            var np = bezier(p1, p1, p4, p4, v);
+            corner.x = np.x;
+            corner.y = np.y;
+            flipMethods._showFoldedPage.call(that, corner);
+
+          },
+          complete: function() {
+            
+            that.trigger('end', [data.opts, true]);
+
+          },
+          duration: turnData.opts.duration,
+          turning: true
+        });
+
+      data.corner = null;
+  },
+
+  moving: function() {
+
+    return 'effect' in this.data();
+  
+  },
+
+  isTurning: function() {
+
+    return this.flip('moving') && this.data().effect.turning;
+  
+  },
+
+  corner: function() {
+    
+    return this.data().f.corner;
+      
+  },
+
+  _eventStart: function(e) {
+
+    var data = this.data().f,
+      turn = data.opts.turn;
+
+    if (!data.corner && !data.disabled && !this.flip('isTurning') &&
+      data.opts.page==turn.data().pagePlace[data.opts.page])
+    {
+
+      data.corner = flipMethods._isIArea.call(this, e);
+
+      if (data.corner && flipMethods._foldingPage.call(this)) {
+
+        this.trigger('pressed', [data.point]);
+        flipMethods._showFoldedPage.call(this, data.corner);
+
+        return false;
+
+      } else
+        data.corner = null;
+
+    }
+
+  },
+
+  _eventMove: function(e) {
+
+    var data = this.data().f;
+
+    if (!data.disabled) {
+
+      e = (isTouch) ? e.originalEvent.touches : [e];
+
+      if (data.corner) {
+
+        var pos = data.parent.offset();
+        data.corner.x = e[0].pageX-pos.left;
+        data.corner.y = e[0].pageY-pos.top;
+        flipMethods._showFoldedPage.call(this, data.corner);
+
+      } else if (data.hover && !this.data().effect && this.is(':visible')) {
+
+        var point = flipMethods._isIArea.call(this, e[0]);
+
+        if (point) {
+
+          if ((data.effect=='sheet' && point.corner.length==2)  || data.effect=='hard') {
+            data.status = 'hover';
+            var origin = flipMethods._c.call(this, point.corner, data.opts.cornerSize/2);
+            point.x = origin.x;
+            point.y = origin.y;
+            flipMethods._showFoldedPage.call(this, point, true);
+          }
+        
+        } else {
+          
+          if (data.status=='hover') {
+            data.status = '';
+            flipMethods.hideFoldedPage.call(this, true);
+          }
+
+        }
+
+      }
+
+    }
+
+  },
+
+  _eventEnd: function() {
+
+    var data = this.data().f,
+      corner = data.corner;
+
+    if (!data.disabled && corner) {
+      if (trigger('released', this, [data.point || corner])!='prevented') {
+        flipMethods.hideFoldedPage.call(this, true);
+      }
+    }
+
+    data.corner = null;
+
+  },
+
+  disable: function(disable) {
+
+    flipMethods.setData.call(this, {'disabled': disable});
+    return this;
+
+  },
+
+  hover: function(hover) {
+    
+    flipMethods.setData.call(this, {'hover': hover});
+    return this;
+
+  },
+
+  peel: function (corner, animate) {
+
+    var data = this.data().f;
+
+    if (corner) {
+
+      if ($.inArray(corner, corners.all)==-1)
+        throw turnError('Corner '+corner+' is not permitted');
+
+      if ($.inArray(corner, flipMethods._cAllowed.call(this))!=-1) {
+
+        var point = flipMethods._c.call(this, corner, data.opts.cornerSize/2);
+        
+        data.status = 'peel';
+
+        flipMethods._showFoldedPage.call(this,
+        {
+          corner: corner,
+          x: point.x,
+          y: point.y
+        }, animate);
+
+      }
+
+
+    } else {
+
+      data.status = '';
+
+      flipMethods.hideFoldedPage.call(this, animate);
+      
+    }
+
+    return this;
+  }
+};
+
+
+// Processes classes
+
+function dec(that, methods, args) {
+
+  if (!args[0] || typeof(args[0])=='object')
+    return methods.init.apply(that, args);
+
+  else if (methods[args[0]])
+    return methods[args[0]].apply(that, Array.prototype.slice.call(args, 1));
+
+  else
+    throw turnError(args[0] + ' is not a method or property');
+
+}
+
+
+// Attributes for a layer
+
+function divAtt(top, left, zIndex, overf) {
+    
+  return {'css': {
+    position: 'absolute',
+    top: top,
+    left: left,
+    'overflow': overf || 'hidden',
+    zIndex: zIndex || 'auto'
+  }
+};
+      
+}
+
+// Gets a 2D point from a bezier curve of four points
+
+function bezier(p1, p2, p3, p4, t) {
+
+  var a = 1 - t,
+    b = a * a * a,
+    c = t * t * t;
+    
+  return point2D(Math.round(b*p1.x + 3*t*a*a*p2.x + 3*t*t*a*p3.x + c*p4.x),
+    Math.round(b*p1.y + 3*t*a*a*p2.y + 3*t*t*a*p3.y + c*p4.y));
+
+}
+  
+// Converts an angle from degrees to radians
+
+function rad(degrees) {
+  
+  return degrees/180*PI;
+
+}
+
+// Converts an angle from radians to degrees
+
+function deg(radians) {
+  
+  return radians/PI*180;
+
+}
+
+// Gets a 2D point
+
+function point2D(x, y) {
+  
+  return {x: x, y: y};
+
+}
+
+// Webkit 534.3 on Android wrongly repaints elements that use overflow:hidden + rotation
+
+function rotationAvailable() {
+  var parts;
+
+  if ((parts = /AppleWebkit\/([0-9\.]+)/i.exec(navigator.userAgent))) {
+    var webkitVersion = parseFloat(parts[1]);
+    return (webkitVersion>534.3);
+  } else {
+    return true;
+  }
+}
+
+// Returns the traslate value
+
+function translate(x, y, use3d) {
+  
+  return (has3d && use3d) ? ' translate3d(' + x + 'px,' + y + 'px, 0px) '
+  : ' translate(' + x + 'px, ' + y + 'px) ';
+
+}
+
+// Returns the rotation value
+
+function rotate(degrees) {
+  
+  return ' rotate(' + degrees + 'deg) ';
+
+}
+
+// Checks if a property belongs to an object
+
+function has(property, object) {
+  
+  return Object.prototype.hasOwnProperty.call(object, property);
+
+}
+
+// Gets the CSS3 vendor prefix
+
+function getPrefix() {
+
+  var vendorPrefixes = ['Moz','Webkit','Khtml','O','ms'],
+  len = vendorPrefixes.length,
+  vendor = '';
+
+  while (len--)
+    if ((vendorPrefixes[len] + 'Transform') in document.body.style)
+      vendor='-'+vendorPrefixes[len].toLowerCase()+'-';
+
+  return vendor;
+
+}
+
+// Detects the transitionEnd Event
+
+function getTransitionEnd() {
+
+  var t,
+    el = document.createElement('fakeelement'),
+    transitions = {
+      'transition':'transitionend',
+      'OTransition':'oTransitionEnd',
+      'MSTransition':'transitionend',
+      'MozTransition':'transitionend',
+      'WebkitTransition':'webkitTransitionEnd'
+    };
+
+  for (t in transitions) {
+    if (el.style[t] !== undefined) {
+      return transitions[t];
+    }
+  }
+}
+
+// Gradients
+
+function gradient(obj, p0, p1, colors, numColors) {
+
+  var j, cols = [];
+
+  if (vendor=='-webkit-') {
+
+    for (j = 0; j<numColors; j++)
+      cols.push('color-stop('+colors[j][0]+', '+colors[j][1]+')');
+    
+    obj.css({'background-image':
+        '-webkit-gradient(linear, '+
+        p0.x+'% '+
+        p0.y+'%,'+
+        p1.x+'% '+
+        p1.y+'%, '+
+        cols.join(',') + ' )'});
+  } else {
+    
+    p0 = {x:p0.x/100 * obj.width(), y:p0.y/100 * obj.height()};
+    p1 = {x:p1.x/100 * obj.width(), y:p1.y/100 * obj.height()};
+
+    var dx = p1.x-p0.x,
+      dy = p1.y-p0.y,
+      angle = Math.atan2(dy, dx),
+      angle2 = angle - Math.PI/2,
+      diagonal = Math.abs(obj.width()*Math.sin(angle2))+Math.abs(obj.height()*Math.cos(angle2)),
+      gradientDiagonal = Math.sqrt(dy*dy + dx*dx),
+      corner = point2D((p1.x<p0.x) ? obj.width() : 0, (p1.y<p0.y) ? obj.height() : 0),
+      slope = Math.tan(angle),
+      inverse = -1/slope,
+      x = (inverse*corner.x - corner.y - slope*p0.x + p0.y)/(inverse-slope),
+      c = {x: x, y: inverse*x - inverse*corner.x + corner.y},
+      segA = (Math.sqrt( Math.pow(c.x-p0.x,2) + Math.pow(c.y-p0.y,2)));
+
+      for (j = 0; j<numColors; j++)
+        cols.push(' '+colors[j][1]+' '+((segA + gradientDiagonal*colors[j][0])*100/diagonal)+'%');
+
+      obj.css({'background-image': vendor+'linear-gradient(' + (-angle) + 'rad,' + cols.join(',') + ')'});
+  }
+}
+
+
+// Triggers an event
+
+function trigger(eventName, context, args) {
+
+  var event = $.Event(eventName);
+  context.trigger(event, args);
+  if (event.isDefaultPrevented())
+    return 'prevented';
+  else if (event.isPropagationStopped())
+    return 'stopped';
+  else
+    return '';
+}
+
+// JS Errors
+
+function turnError(message) {
+
+  function TurnJsError(message) {
+    this.name = "TurnJsError";
+    this.message = message;
+  }
+
+  TurnJsError.prototype = new Error();
+  TurnJsError.prototype.constructor = TurnJsError;
+  return new TurnJsError(message);
+
+}
+
+// Find the offset of an element ignoring its transformation
+
+function findPos(obj) {
+
+  var offset = {top: 0, left: 0};
+
+  do{
+    offset.left += obj.offsetLeft;
+    offset.top += obj.offsetTop;
+  } while ((obj = obj.offsetParent));
+
+  return offset;
+
+}
+
+// Checks if there's hard page compatibility
+// IE9 is the only browser that does not support hard pages
+
+function hasHardPage() {
+  return (navigator.userAgent.indexOf('MSIE 9.0')==-1);
+}
+
+// Request an animation
+
+window.requestAnim = (function() {
+  return window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame ||
+    window.oRequestAnimationFrame ||
+    window.msRequestAnimationFrame ||
+    function(callback) {
+      window.setTimeout(callback, 1000 / 60);
+    };
+
+})();
+
+// Extend $.fn
+
+$.extend($.fn, {
+
+  flip: function() {
+    return dec($(this[0]), flipMethods, arguments);
+  },
+
+  turn: function() {
+    return dec($(this[0]), turnMethods, arguments);
+  },
+
+  transform: function(transform, origin) {
+
+    var properties = {};
+    
+    if (origin)
+      properties[vendor+'transform-origin'] = origin;
+    
+    properties[vendor+'transform'] = transform;
+  
+    return this.css(properties);
+
+  },
+
+  animatef: function(point) {
+
+    var data = this.data();
+
+    if (data.effect)
+      data.effect.stop();
+
+    if (point) {
+
+      if (!point.to.length) point.to = [point.to];
+      if (!point.from.length) point.from = [point.from];
+
+      var diff = [],
+        len = point.to.length,
+        animating = true,
+        that = this,
+        time = (new Date()).getTime(),
+        frame = function() {
+
+          if (!data.effect || !animating)
+            return;
+
+          var v = [],
+            timeDiff = Math.min(point.duration, (new Date()).getTime() - time);
+
+          for (var i = 0; i < len; i++)
+            v.push(data.effect.easing(1, timeDiff, point.from[i], diff[i], point.duration));
+
+          point.frame((len==1) ? v[0] : v);
+
+          if (timeDiff==point.duration) {
+            delete data['effect'];
+            that.data(data);
+            if (point.complete)
+              point.complete();
+          } else {
+            window.requestAnim(frame);
+          }
+        };
+
+      for (var i = 0; i < len; i++)
+        diff.push(point.to[i] - point.from[i]);
+
+      data.effect = $.extend({
+        stop: function() {
+          animating = false;
+        },
+        easing: function (x, t, b, c, data) {
+          return c * Math.sqrt(1 - (t=t/data-1)*t) + b;
+        }
+      }, point);
+
+      this.data(data);
+
+      frame();
+
+    } else {
+      
+      delete data['effect'];
+
+    }
+  }
+});
+
+// Export some globals
+
+$.isTouch = isTouch;
+$.mouseEvents = mouseEvents;
+$.cssPrefix = getPrefix;
+$.cssTransitionEnd = getTransitionEnd;
+$.findPos = findPos;
+
+})(jQuery);

+ 408 - 0
src/views/analysisReport/components/benchTaskSelectSchool.vue

@@ -0,0 +1,408 @@
+<template>
+
+<el-dialog :title="title" :visible.sync="localShowDialog" width="60%">
+    <div class="dialog_center padding_20 page_search">
+        <div class="search_content">
+            <div class="content_left"> 
+                <el-select  v-model="enrollmentYear" @change="ChangeFilter" placeholder="入学年份" class="select_width">
+                    <el-option label="全部学届" value=""></el-option>
+                    <el-option
+                        v-for="item in graduatesList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                    </el-option>
+                </el-select>
+                <el-select  v-model="levelCode"  @change="ChangeFilter" placeholder="选择学段" class="select_width">
+                    <el-option label="全部学段" value=""></el-option>
+                    <el-option
+                        v-for="item in levelsList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                    </el-option>
+                </el-select>
+                <el-select  v-model="gradeCode"  @change="ChangeFilter" placeholder="选择年级" class="select_width" >
+                    <el-option label="全部年级" value=""></el-option>
+                    <el-option
+                        v-for="item in gradeList"
+                        :key="item.gradeCode"
+                        :label="item.gradeName"
+                        :value="item.gradeCode">
+                    </el-option>
+                </el-select>
+                <el-input class="input_width" v-model="keyWord" @input="ChangeFilter"   placeholder="考试编号、考试名称"> 
+                    <el-button @click="ChangeFilter" slot="append" icon="el-icon-search"></el-button>
+                </el-input>
+            </div>
+        </div>
+        <div class="page_jg"></div>
+        <FiltersItem :filtersData="filtersData" @selectItem="ChangeFilters" />
+        <div class="page_jg"></div>
+        <div class="page_table table_row_42">
+            <el-table ref="multipleTable" :key="benchTaskSelectKey" :data="tableData" border stripe row-key="id"  @row-click="HandleClickChange" @selection-change="HandleSelectionChange" @select="HandleSelection">
+                <el-table-column  width="55" align="center" v-if="!isMultiple">
+                    <template slot-scope="scope">
+                        <div class="table_row_checked" >
+                            <div class="icon_checked" v-if="radioId == scope.row.id">
+                                <div class="checked_bg"></div>
+                            </div>
+                            <div v-else class="icon_item"></div>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column type="selection" reserve-selection align="center"  width="55" v-if="isMultiple">
+
+                </el-table-column>
+                <el-table-column align="center" prop="examDate" width="120" label="考试日期" >
+                    
+                </el-table-column>
+                <el-table-column align="center" prop="examName"  label="考试名称" show-overflow-tooltip>
+                </el-table-column>
+                <el-table-column align="center" prop="gradeName" width="100" label="年级" >
+                </el-table-column>
+                <el-table-column align="center" prop="examType" width="100" label="考试类型">
+                </el-table-column>
+                <el-table-column align="center" prop="courseNames" min-width="100" label="科目" show-overflow-tooltip>
+                </el-table-column>
+            </el-table>
+        </div>
+        <div class="page_pagination" v-if="pageInfo.total>10">
+            <el-pagination
+                @current-change="ChangePage"
+                background
+                layout="prev,pager,next"
+                :page-size="pageInfo.pageSize"
+                :current-page="pageInfo.pageNum"
+                :total="pageInfo.total">
+            </el-pagination>
+        </div>
+    </div>
+    <div slot="footer" class="dialog-footer">
+        <el-button @click="localShowDialog = false">取 消</el-button>
+        <el-button type="primary" @click="EnterSubmit()">确 定</el-button>
+    </div>
+</el-dialog>
+
+</template>
+<script>
+import FiltersItem from "@/components/FiltersItem_ruoyan.vue";
+  export default {
+    components: { FiltersItem },
+    props: {
+      showDialog: {
+        type: Boolean,
+        default: false,
+      },//是否显示弹窗
+
+      title:{
+        type: String,
+        default: '对标任务选择',
+      },//标题
+
+      isMultiple:{
+        type: Boolean,
+        default: true,
+      },//是否多选
+      benchTaskSelectKey: {
+        type: Number,
+        default: 0,
+      },
+      before: {
+        type: Number,
+        default: undefined,
+      },// 是否添加before参数
+    },
+    watch: {
+        showDialog(newVal) {
+            this.localShowDialog = newVal; // 监听外部传入的 showDialog 变化
+            if(newVal) {
+                this.enrollmentYear = this.reportExamItem.graduates.toString();//届
+                this.levelCode = this.reportExamItem.levelCode.toString();//学段
+                this.gradeCode = this.reportExamItem.gradeCode;//年级
+                this.selectList = this.$store.state.report.lastExamSelectIds;
+                this.GetLocalFilterData();//从本地读取筛选数据
+                this.GetPreviousExamPageList()//获取数据
+            }
+            
+        },
+        localShowDialog(newVal) {
+            this.$emit('update:showDialog', newVal); // 通知父组件显示状态变化
+        },
+    },
+    data() {
+      return {
+        localShowDialog: this.showDialog, // 使用本地数据来控制显示
+        enrollmentYear:'',//学届
+        levelCode:'',//学段
+        gradeCode:'',//年级
+        keyWord:'',//搜索关键字
+        examType:'',//考试类型
+        examTypeList:[],//考试类型list
+        graduatesList:[],//选择学届list
+        levelsList:[],//选择学段list
+        gradeList:[],//选择年级原数组list
+        gradeAllList:[],//选择年级所有list
+        filtersData:[
+            {
+                name: '考试类型',
+                value: '',
+                list: [
+                    {
+                        label: '全部',
+                        value: "",
+                    },
+
+                ],
+            },
+        ],//筛选数据
+        tableData:[],//表格数据
+
+        pageInfo: {
+          pageSize: 10,
+          pageNum: 1,
+          total: 0,
+        },//分页数据
+
+        radioId:'',//单选的考生id
+        radioName:'',//单选的考生name
+        selectList:[],//多选的考试
+
+
+      };
+    },
+    computed:{
+        reportExamItem() {
+            // let item = JSON.parse(localStorage.getItem('reportExamItem'));
+            return this.$store.state.report.examSelectItem;
+        },//分析报告公共参数变量
+    },
+    
+    created() {
+        // this.enrollmentYear = this.reportExamItem.graduates.toString();//届
+        // this.levelCode = this.reportExamItem.levelCode.toString();//学段
+        // this.gradeCode = this.reportExamItem.gradeCode;//年级
+        this.selectList = this.$store.state.report.lastExamSelectIds;
+        console.log(this.selectList)
+        this.GetLocalFilterData();//从本地读取筛选数据
+        this.GetPreviousExamPageList()
+    },
+    methods: {
+
+        //从本地读取数据
+        GetLocalFilterData() {
+            this.examType = ''
+            if(this.$store.state.report.filterDataObject != null) {
+                let filterDataObject = this.$store.state.report.filterDataObject;
+                //届别
+                this.graduatesList = filterDataObject.graduatesList;
+                console.log(this.graduatesList)
+                this.filtersData=[
+                    {
+                        name: '考试类型',
+                        value: '',
+                        list: [
+                            {
+                                label: '全部',
+                                value: '',
+                            }
+                        ],
+                    }
+                ];//筛选数据
+                
+                //学段
+                this.levelsList =  filterDataObject.levelsList;
+                console.log("打印学段列表",this.levelsList);
+                //年级list
+                this.gradeAllList = filterDataObject.gradeList;
+                console.log("this.gradeAllList",this.gradeAllList,this.levelCode);
+                this.gradeList = this.gradeAllList.filter(grade => grade.levelCode == this.levelCode);
+                //examTypeList考试类型
+                this.examTypeList = filterDataObject.examTypeList;
+                this.examTypeList.forEach(item => {
+                    let obj = {
+                        label: item.examName,
+                        value: item.examType,
+                    };
+                    this.filtersData[0].list.push(obj);
+                })
+            }
+        },
+
+        //获取筛选的数据
+        ChangeFilters(e) {
+            // console.log('筛选切换事件', e);
+            this.filtersData[e.index].value = e.value;
+            this.examType = e.value;
+            this.ChangeFilter();
+        },
+        //多选选择行
+        HandleSelectionChange(val) {
+            // console.log("打印选择行",val);
+            // if(this.isMultiple) {
+            //     this.selectList = val;
+            // }
+            // console.log("打印选择的selectList", this.selectList);
+        },
+        HandleSelection(e, row){
+            if(this.isMultiple) {
+                const index = this.selectList.indexOf(row.id);
+                if(index == -1){
+                    this.selectList.push(row.id)
+                }else{
+                    this.selectList.splice(index,1)
+                }
+                // this.$refs.multipleTable.toggleRowSelection(e);
+            }
+        },
+        //行选择事件
+        HandleClickChange(e) {
+            if(this.isMultiple) {
+                const index = this.selectList.indexOf(e.id);
+                if(index == -1){
+                    this.selectList.push(e.id)
+                }else{
+                    this.selectList.splice(index,1)
+                }
+                this.$refs.multipleTable.toggleRowSelection(e);
+            }else{
+                this.radioId = e.id;
+                this.radioName = e.examName;
+            }
+        },
+        //筛选
+        ChangeFilter() {
+            this.pageInfo.pageNum=1;
+            this.GetPreviousExamPageList();//分页获取历次考试列表
+        },
+        //分页
+        ChangePage(num) {
+            this.pageInfo.pageNum = num;
+            this.GetPreviousExamPageList();//分页获取历次考试列表
+        },
+
+        //分页获取历次考试列表
+        GetPreviousExamPageList() {
+            return
+            let params = {
+                examId: this.$store.state.report.examId, // 考试id
+                examLevel:this.$store.state.report.examLevel,
+                graduates: this.enrollmentYear, // 学届
+                levelCode: this.levelCode, // 学段
+                gradeCode: this.gradeCode, // 年级
+                word: this.keyWord, // 模糊查询
+                examType: this.examType, // 考试类型
+                courseCode: this.$store.state.report.filterObject.subjectCode, // 考试科目
+                examDate: '', // 考试日期
+                pageNum: this.pageInfo.pageNum,//页码
+                pageSize: this.pageInfo.pageSize,//每页内容
+            };
+            
+            // 根据before属性动态添加或删除before参数
+            if (this.before !== undefined) {
+                params.before = this.before;
+            }
+            //获取历次考试列表
+            this.$api.reportSchool.selectPreviousAnalysisExamPageList(params).then(res => {
+                if(res.code==200 && res.data) {
+                    this.tableData = res.data.records || [];
+                    this.pageInfo.total = parseInt(res.data.total) || 0;//总条数
+                } else {
+                    this.tableData = [];
+                    this.pageInfo.total = 0
+                }
+
+                this.checkSelect()
+            })
+        },
+        checkSelect() {
+            this.$nextTick(() => {
+                if(this.isMultiple) {
+                    let selectId = this.$store.state.report.lastExamSelectIds;
+                    this.tableData.forEach(row => {
+                        if (selectId.includes(row.id)) {
+                            this.$refs.multipleTable && this.$refs.multipleTable.toggleRowSelection(row, true);
+                        }
+                    });
+                }else{
+                    this.radioId = this.$store.state.report.lastExamRadioId
+                    this.radioName = this.$store.state.report.lastExamRadioName
+                }
+            });
+        },
+
+        //确定选择事件
+        EnterSubmit() {
+            console.log("确定选择事件",this.radioId);
+            console.log("确定选择事件",this.selectList);
+            let idList=[];
+            console.log(this.isMultiple);
+            if(this.isMultiple) {
+                
+                //多选  
+                // this.selectList.forEach(item=>{
+                //     idList.push(item.id);
+                // });
+                if(this.selectList.length > 19) {
+                    this.$message.warning('历次考试对比最多支持20场!');
+                    return
+                }
+                this.$store.commit("report/SetLastExamSelectIds", this.selectList)
+            } else {
+                //单选
+                idList.push(this.radioId);
+                this.$store.commit("report/SetLastExamRadioId", this.radioId)
+                this.$store.commit("report/SetLastExamRadioName", this.radioName ?? '')
+            }
+            this.$emit("GetSelectId");
+            this.localShowDialog = false;
+        },
+
+       
+    },
+  };
+  </script>
+  
+  <style lang="scss" scoped>
+  :deep(.el-table__header) .el-checkbox {
+    display: none;
+  }
+  .table_row_checked
+  {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .icon_checked
+    {
+        width: 20px;
+        height: 20px;
+        border-radius: 50%;
+        background-color: #fff;
+        border:1px solid #2E64FA;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .checked_bg
+        {
+            width: 12px;
+            height: 12px;
+            border-radius: 50%;
+            background-color: #2E64FA;
+
+        }
+    }
+
+    .icon_item
+    {
+        width: 20px;
+        height: 20px;
+        background: #FFFFFF;
+        border-radius: 50%;
+        border: 1px solid #00000020;
+    }
+  }
+  </style>
+  

+ 1 - 1
src/views/analysisReport/components/dCharts/GaugeChart.vue

@@ -180,7 +180,7 @@ export default {
           }],
           endAngle: this.endAngle || 360,
           startAngle: this.startAngle || 0,
-          clockwise: true,
+          clockwise: true,//仪表盘刻度是否是顺时针增长
           axisLine: { //仪表盘轴
             show: true,
             roundCap: false,//是否在两端显示成圆形

+ 130 - 80
src/views/analysisReport/components/dCharts/barChart.vue

@@ -4,7 +4,7 @@
     </div>
 </template>
 <script>
-import throttle from 'lodash/throttle';
+import _ from 'lodash'
 import * as echarts from 'echarts'; 
 
   export default{
@@ -72,9 +72,13 @@ import * as echarts from 'echarts';
       },//标题
 
       average:{
-        type:[String, Number],
+        type:[String, Number,Array],
         default:'0'
       },//平均分辅助线
+      markLineData:{
+        type:Array,
+        default:()=>[]
+      },//平均分辅助线
       isShowMarkLine:{
         type:Boolean,
         default:true
@@ -101,15 +105,56 @@ import * as echarts from 'echarts';
         type:[String,Number],
         default:''
       },//满分
+      gridLeft:{
+        type:Number,
+        default:40
+      },
+      gridRight:{
+        type:Number,
+        default:60
+      },
+      gridTop:{
+        type:Number,
+        default:20
+      },
+      fontSize:{//字体大小
+        type:[Number,String],
+        default:''
+      },
+      fontColor:{
+        type:String,
+        default:''
+      },
+      showDataZoom:{//是否显示滚动条
+        type:Boolean,
+        default:true
+      },
+      barMaxWidth:{
+        type:Number,
+        default:50
+      },
+      barMinWidth:{
+        type:Number,
+        default:20
+      }
     },
     watch:{
         datay:{
             handler:'LoadEchart',
             deep:true,
         },//数值变化时重新加载图表
+        markLineData:{
+            handler:'LoadEchart',
+            deep:true,
+        },//数值变化时重新加载图表
+    },
+    computed:{
+        getCompareAnalysis(){
+            return this.$global.getCompareAnalysis()
+        },
     },
     created(){
-        // window.addEventListener('resize', this.handleResize);//创建时增加监听窗口变化事件
+        window.addEventListener('resize', this.handleResize);//创建时增加监听窗口变化事件
     },
     beforeDestroy() {
         // 组件销毁移除监听 防止内存泄漏
@@ -131,7 +176,7 @@ import * as echarts from 'echarts';
 
             const colors = [];
             const xColors=[];
-            console.log("打印x轴数据",this.datax);
+            // console.log("打印x轴数据",this.datax);
             const rightAnswerValue = this.answerValue || '';//正确答案
             for (let i = 0; i < this.datax.length; i++) {
                 if(this.color){
@@ -144,13 +189,13 @@ import * as echarts from 'echarts';
             }
 
             // 初始化 xAxisLabels 数组
-            this.xAxisLabels = this.datax.map((label, index) => ({
-                value: label,
-                textStyle: {
-                    color: xColors[index],
-                    fontSize: 14
-                }
-            }));
+            // this.xAxisLabels = this.datax.map((label, index) => ({
+            //     value: label,
+            //     textStyle: {
+            //         color: xColors[index],
+            //         fontSize: 14
+            //     }
+            // }));
             const markPointColor=this.color;
             //devicePixelRatio: 2  2表示每个逻辑像素对应 2个物理像素。这会进一步提高图表的清晰度,适用于更高分辨率的屏幕  解决高分辨率显示器文字模糊的问题
             this.echart=echarts.init(this.$refs.bar_echart,{devicePixelRatio: 2});//获取echarts容器  如果组件同一个页面多处使用 这里不能用id获取实例
@@ -166,14 +211,39 @@ import * as echarts from 'echarts';
             // console.log("打印datay", this.datay);
 
             //计算最大值和最小值
-            const datay = this.datay.filter(num => !isNaN(num));//// 过滤掉NaN值
+            const datay = this.datay.filter(num => !isNaN(num))
             const maxValue=Math.max(...datay);
             const minValue=Math.min(...datay);
-            const average=parseFloat(this.average) || 0;
+            let average = 0, markLineData = [],maxAverage = [];
+            if(Object.prototype.toString.call(this.average) === '[object Array]'){
+                average = this.average;
+                this.average.forEach(item=>{
+                    const itemAverage = parseFloat(item) || 0
+                    if(item > 0) {
+                        markLineData.push({symbol:'circle',type: 'value', name: '', yAxis: itemAverage})
+                    }
+                })
+                maxAverage = average.length ? Math.max(...average) : 0;
+            }else if(this.markLineData && this.markLineData.length){
+                average = [];
+                this.markLineData.forEach((item,index)=>{
+                    const itemAverage = parseFloat(item.value) || 0
+                    average.push(itemAverage)
+                    if(item.isShow){
+                        markLineData.push({symbol:'circle',type: 'value', name: '', yAxis: itemAverage,lineStyle: {color: this.getCompareAnalysis[index]},label:{color:this.getCompareAnalysis[index]}})
+                    }
+                })
+                maxAverage = average.length ? Math.max(...average) : 0;
+            }else{
+                average=parseFloat(this.average) || 0;
+                maxAverage = parseFloat(this.average) || 0;
+                markLineData = average!=0?[{symbol:'circle',type: 'value', name: '', yAxis: average}]:[];
+            }
+            
+            // console.log("打印最大值", maxValue);
             // console.log("打印最小值", minValue);
             // console.log("打印平均分", average);
             const nearestValue = this.GetMaxValue(maxValue);//向上取整为最接近10的数字
-
             // console.log("打印最大值的",nearestValue)
             let yAxisUnit=this.showNuitY && this.unit == '%' ? this.unit : '';
             let splitArea=this.showSplitArea?{ "show": true, areaStyle: {"color": ["#fafafa", "#ffffff"]}}:{}
@@ -267,13 +337,13 @@ import * as echarts from 'echarts';
                 // },
                
                 grid: {
-                    left: 40,
-                    right: 20,
-                    top: 20,
+                    left: this.gridLeft,
+                    right: this.gridRight,
+                    top: this.gridTop,
                     bottom: 0,
                     containLabel: true, //用于控制网格区域是否包含坐标轴的标签  true 可以确保所有标签都能完整显示,不会被裁剪。
                 },
-                dataZoom: singleSeriesWidth < barMinWidth ? dataZoom : null,
+                dataZoom: this.showDataZoom?(singleSeriesWidth < barMinWidth ? dataZoom : null):null,
                 xAxis: [
                     {
                         type: 'category',
@@ -294,7 +364,7 @@ import * as echarts from 'echarts';
                                 // console.log("文字宽度",valueWidth);
                                 // console.log("单系列宽度",singleSeriesWidth);
                                 if(valueWidth > singleSeriesWidth) {
-                                    if (singleSeriesWidth < 80) {
+                                    if (singleSeriesWidth < 100) {
                                         if(valueWidth > 80) {
                                             // let maxLength = Math.floor(80 / 14) - 1;
                                             return value.slice(0, 2) + '...' + value.slice(-3)
@@ -309,12 +379,12 @@ import * as echarts from 'echarts';
                                     return value;
                                 }
                             },
-                            color: '#666666',//y轴文字颜色
+                            color: this.fontColor || '#666666',//y轴文字颜色
                             // width: 80, // 控制每个标签最大宽度,超出则截断
                             // overflow: 'truncate', // 超出宽度显示省略号
                             // ellipsis: '...', // 自定义省略符号
                             // margin: 20,
-                            fontSize: 14,
+                            fontSize: this.fontSize?this.fontSize:14,
                             // rich: {
                             //     // 定义省略样式(可选)
                             //     text: {
@@ -335,11 +405,11 @@ import * as echarts from 'echarts';
                     type: 'value',
                     // name: '进度(%)',
                     axisLabel: {
-                        color: '#666666',//y轴文字颜色
-                        fontSize: 14,
+                        color: this.fontColor || '#666666',//y轴文字颜色
+                        fontSize: this.fontSize?this.fontSize:14,
                         formatter: '{value}'+yAxisUnit, // 添加 % 符号
                     },
-                    max: average > nearestValue ? average : nearestValue,//固定y轴最高值为最大值的向上取整
+                    max: maxAverage > nearestValue ? maxAverage : nearestValue,//固定y轴最高值为最大值的向上取整
                     // splitNumber: 10, // 设置刻度线分段数
                     // interval: 0, // 确保所有刻度标签都显示
                     // splitLine: {
@@ -371,8 +441,8 @@ import * as echarts from 'echarts';
                             color: 'red'
                         },//柱子的背景色
                         // barMinHeight:this.isClick?30:0,//柱子最小高度
-                        barMaxWidth: 50, // 设置柱状图的最大宽度
-                        barMinWidth: 20,
+                        barMaxWidth: this.barMaxWidth, // 设置柱状图的最大宽度
+                        barMinWidth: this.barMinWidth,
                         // barWidth: '44px', // 设置柱状图的宽度,可以是百分比或具体数值
                         itemStyle: {
                             color: function(params) {
@@ -422,16 +492,16 @@ import * as echarts from 'echarts';
                         data: this.datay.map(value => ({
                             value: value,
                             label: {
-                                // show: this.showMarkPoint ? (value === 0 || value === maxValue || value === minValue) : singleSeriesWidth > 22,//数值为0不显示
+                                show: this.showMarkPoint ? (value === 0 || value === maxValue || value === minValue) : singleSeriesWidth > 26,//数值为0不显示
                                 position: "top",
-                                fontSize: 14,
+                                fontSize: this.fontSize?this.fontSize:14,
                                 formatter: '{c}'+this.unit, // 显示的格式
                                 color:"#666"
                                 // color: (value == maxValue)? "#3BA272": (value == minValue)? "#EE6666": "#666"
                             },
                         })),
                         markLine: {
-                            symbol:average==0?['none', 'none']:['circle','arrow'],//设置标记线样式  圆形'circle', 矩形'rect', 圆角矩形'roundRect', 三角形'triangle', 菱形'diamond', 图钉行'pin', 箭头行'arrow', 'none'
+                            // symbol:average==0?['none', 'none']:['circle','arrow'],//设置标记线样式  圆形'circle', 矩形'rect', 圆角矩形'roundRect', 三角形'triangle', 菱形'diamond', 图钉行'pin', 箭头行'arrow', 'none'
                             symbolSize:[8,8],//设置标记线样式大小 宽高
                             symbolOffset: [
                                 [0, 0],    // 起始标记偏移
@@ -440,7 +510,7 @@ import * as echarts from 'echarts';
                             label:{
                                 // formatter: '{b}: {c}',
                                 color:'#F56C6C',
-                                fontSize: 15,
+                                fontSize: this.fontSize?this.fontSize:15,
                                 formatter: '{c}'+this.unit,
                                 position: 'end',//'start', 'middle', 'end', 'insideStartTop', 'insideStartBottom', 'insideMiddleTop', 'insideMiddleBottom', 'insideEndTop', 'insideEndBottom'
                                 // distance:[-10,-40],//标签与线之间的间距。如果是数组,第一项为横向间距,第二项为纵向间距。如果是数字,则表示横向纵向使用相同的间距
@@ -448,14 +518,8 @@ import * as echarts from 'echarts';
                             tooltip: {
                                formatter:{},
                             },
-                            data: average!=0?[
-                                { 
-                                    type: 'value', 
-                                    name: '', 
-                                    yAxis: average
-                                }
-                            ]:[],
-                            lineStyle: {
+                            data: markLineData,
+                            lineStyle: this?.markLineData?.length?null:{
                                 color: '#F56C6C'
                             },//辅助线的颜色
                         
@@ -519,6 +583,7 @@ import * as echarts from 'echarts';
             //     option.xAxis[0].axisLabel.rich[`a${index}`] = label.textStyle;
             // });
             this.echart.setOption(option);
+
             //是否可以点击
             if(this.isClick)
             {
@@ -537,27 +602,27 @@ import * as echarts from 'echarts';
                     {
                         
                         // 更新 xAxisLabels 数组
-                        this.xAxisLabels = this.datax.map((label, index) => ({
-                            value: label,
-                            textStyle: {
-                                color: index === params.dataIndex ? '#2E64FA' : xColors[index],
-                                fontSize: 14
-                            }
-                        }));
+                        // this.xAxisLabels = this.datax.map((label, index) => ({
+                        //     value: label,
+                        //     textStyle: {
+                        //         color: index === params.dataIndex ? '#2E64FA' : xColors[index],
+                        //         fontSize: 14
+                        //     }
+                        // }));
                         // 设置 rich 样式
-                        this.xAxisLabels.forEach((label, index) => {
-                            this.echart.setOption({
-                                xAxis: [
-                                    {
-                                        axisLabel: {
-                                            rich: {
-                                                [`a${index}`]: label.textStyle
-                                            }
-                                        }
-                                    }
-                                ]
-                            });
-                        });
+                        // this.xAxisLabels.forEach((label, index) => {
+                        //     this.echart.setOption({
+                        //         xAxis: [
+                        //             {
+                        //                 axisLabel: {
+                        //                     rich: {
+                        //                         [`a${index}`]: label.textStyle
+                        //                     }
+                        //                 }
+                        //             }
+                        //         ]
+                        //     });
+                        // });
                         this.echart.setOption({
                             series: [
                                 {
@@ -621,26 +686,7 @@ import * as echarts from 'echarts';
                     },
                 });
             }
-            // 获取grid区域偏移量
-            const gridRect = this.echart.getModel().getComponent('grid').coordinateSystem.getRect();
-            const dataLens = this.datax.length ?? 0;
-            const itemBarWidth = (gridRect.width / dataLens - 0) * 0.7;//每个柱子的真实宽度
-            this.echart.setOption({
-                series: {
-                    data: this.datay.map(value => ({
-                        value: value,
-                        label: {
-                            show: itemBarWidth > 23,
-                            position: "top",
-                            fontSize: 14,
-                            formatter: '{c}'+this.unit, // 显示的格式
-                            color:"#666"
-                            // color: (value == maxValue)? "#3BA272": (value == minValue)? "#EE6666": "#666"
-                        },
-                    }))
-                }
-            });
-            window.addEventListener("resize", this.handleResize); //创建时增加监听窗口变化事件
+            
         },
 
         //获取最大值  计算规则  
@@ -798,10 +844,14 @@ import * as echarts from 'echarts';
         },
 
         // 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
-        handleResize: throttle(function () {
+        handleResize: _.throttle(function() {
+            
             this.$nextTick(() => {
-                this.echart.resize();
+               
+                this.LoadEchart();
+                
             });
+            
         }, 500), // 节流 500 毫秒内最多执行一次
     }
   }

+ 36 - 12
src/views/analysisReport/components/dCharts/differenceChart.vue

@@ -63,6 +63,30 @@ export default {
         return ["#3BA272", "#EE6666"];
       },
     },
+    gridLeft:{
+      type:Number,
+      default:40
+    },
+    gridRight:{
+      type:Number,
+      default:60
+    },
+    gridTop:{
+      type:Number,
+      default:20
+    },
+    fontSize:{//字体大小
+      type:[Number,String],
+      default:''
+    },
+    fontColor:{
+      type:String,
+      default:''
+    },
+    showDataZoom:{//是否显示滚动条
+      type:Boolean,
+      default:true
+    }
   },
   watch: {
     datay: {
@@ -168,8 +192,8 @@ export default {
                 return `${this.$global.getFormatNumber(value).toString()+unit}`;
               }
             },
-            color: "#666",
-            fontSize: 14,
+            color: this.fontColor || "#666",
+            fontSize: this.fontSize?this.fontSize:14,
           },
           itemStyle: {
             color: numValue > 0 ? this.color[0] : this.color[1],
@@ -212,13 +236,13 @@ export default {
           },
         },
         grid: {
-          left: 40,
-          right: 20,
-          top: 20,
+          left: this.gridLeft,
+          right: this.gridRight,
+          top: this.gridTop,
           bottom:0,
           containLabel: true, //用于控制网格区域是否包含坐标轴的标签  true 可以确保所有标签都能完整显示,不会被裁剪。
         },
-        dataZoom: singleSeriesWidth < barMinWidth ? dataZoom : null,
+        dataZoom: this.showDataZoom?(singleSeriesWidth < barMinWidth ? dataZoom : null):null,
         xAxis: [
           {
             type: "category",
@@ -230,8 +254,8 @@ export default {
               interval: 0,
               margin: 20,
               rotate: singleSeriesWidth < 80 ? 45 : 0,
-              fontSize: 14,
-              color: "#666",
+              fontSize: this.fontSize?this.fontSize:14,
+              color: this.fontColor || "#666",
               fontWeight: 400,
               formatter:function(value) 
               { 
@@ -275,9 +299,9 @@ export default {
               // console.log("打印y轴的value",totalValue);
               return  totalValue.toString()+ unit;
             },
-            color: "#666",
+            color: this.fontColor || "#666",
             // interval: 0,
-            fontSize: 14,
+            fontSize: this.fontSize?this.fontSize:14,
           },
           max:maxValue,
           min:minValue,
@@ -310,8 +334,8 @@ export default {
               focus: "series",
             },
             barMinHeight:1,//设置柱状图的最小高度
-            barMaxWidth: 50, // 设置柱状图的最大宽度
-            barMinWidth: 20, ////设置柱状图的最小宽度
+            barMaxWidth: 50, // this.showDataZoom?50:30 设置柱状图的最大宽度
+            barMinWidth: this.showDataZoom?20:1, ////设置柱状图的最小宽度
             data: processedData,
           },
         ],

+ 46 - 22
src/views/analysisReport/components/dCharts/lineChart.vue

@@ -5,7 +5,7 @@
                 显示全部
             </el-checkbox>
         </div>
-        <div ref="line_chart" class="chart_box"></div>
+        <div ref="line_chart" class="chart_box" :style="{height:reportHeight || '400px'}"></div>
     </div>
 </template>
 <script>
@@ -32,7 +32,14 @@ export default {
             type: Number,
             default: 0,
         },//grid右间距
-
+        gridTop: {
+            type: [Number,String],
+            default: '',
+        },
+        legendWidth: {
+            type: [Number,String],
+            default: 'auto',
+        },
         showCheckBox: {
             type: Boolean,
             default: true,
@@ -106,7 +113,10 @@ export default {
             type:Boolean,
             default:false,
         },//是否显示柱状图label  默认显示
-
+        labelColor:{
+            type: String,
+            default:'',
+        },//柱状图label 颜色
         showMarkPoint:{
             type: Boolean,
             default:false,
@@ -126,8 +136,22 @@ export default {
             type:Number,
             default:0
         },
-
-
+        gridLeft:{
+            type:Number,
+            default:20
+        },
+        fontSize:{//字体大小
+            type:[Number,String],
+            default:''
+        },
+        fontColor:{
+            type:String,
+            default:''
+        },
+        reportHeight:{//报告高度
+            type:String,
+            default:''
+        }
 
     },
     watch: {
@@ -308,7 +332,7 @@ export default {
                 legend: {
                     show: this.title.length ? true : false,//是否显示图例
                     left: this.showCheckBox ? "110px" : "0px",//图例距左边距离  有显示全部的 默认110px
-               
+                    width:this.legendWidth,
                     itemGap: 20,//图例间距
                     itemHeight: 10,//图例的高度
                     itemWidth: 25,//图例的宽度
@@ -332,9 +356,9 @@ export default {
                     // icon:'emptyCircle',//标记类型 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
                 },
                 grid: {
-                    left: 40,
-                    right: 20,
-                    top: gridTop,
+                    left: this.gridLeft,
+                    right: this.gridRight,
+                    top: this.gridTop || gridTop,
                     bottom:0,
                     containLabel: true, //用于控制网格区域是否包含坐标轴的标签  true 可以确保所有标签都能完整显示,不会被裁剪。
                 },
@@ -362,8 +386,8 @@ export default {
                     axisLabel: {
                         interval: 0,//0 显示所有刻度
                         rotate: singleSeriesWidth < 60 ? 45 : 0,
-                        fontSize: 14,
-                        color: "#666",
+                        fontSize: this.fontSize?this.fontSize:14,
+                        color: this.fontColor || "#666",
                         fontWeight: 400,
                         formatter:function(value) {
                             
@@ -403,8 +427,8 @@ export default {
                         margin: 20,
                         formatter: '{value}' + this.unit,
                         interval: 0, // 显示所有 label
-                        fontSize: 14,
-                        color: "#666",
+                        fontSize: this.fontSize?this.fontSize:14,
+                        color: this.fontColor || "#666",
                         fontWeight: 400
                     },
                     splitNumber: 5,//固定显示5个刻度  非强制 不一定生效
@@ -430,16 +454,16 @@ export default {
                         },// 网格线设置隔行背景色
                     },
                     inverse:this.yInverse,
-                    min:this.showBackground?0: (val) => {
+                    // min:this.showBackground?0: (val) => {
 
                         
-                        if(val.min > 0 || val.min < -10) {
-                            // return Math.floor(val.min / 10) * 10
-                            return Math.floor(val.min)
-                        }else {
-                            return val.min + (val.min % 10)
-                        }
-                    },
+                    //     if(val.min > 0 || val.min < -10) {
+                    //         // return Math.floor(val.min / 10) * 10
+                    //         return Math.floor(val.min)
+                    //     }else {
+                    //         return val.min + (val.min % 10)
+                    //     }
+                    // },
                     // max:yAxisMax,//固定x轴最高值位100
                     // max: this.unit != '%' ? (val) => {
                     //     // console.log(val.max)
@@ -471,7 +495,7 @@ export default {
                     symbolSize:8,//折线点大小
                     label: { 
                         show: this.isShowLabel, //是否显示数值
-                        color: this.colors[index], 
+                        color: this.labelColor || this.colors[index], 
                         formatter: "{c}" + this.unit 
                     },
                     smooth: this.isSmooth,//设置折线平滑

+ 66 - 14
src/views/analysisReport/components/dCharts/radarCharts.vue

@@ -6,7 +6,7 @@
       </el-checkbox>
     </div>
     <!-- 雷达图 -->
-    <div ref="radarChart" class="chart_box" :style="{ height: `${height}px` }"></div>
+    <div ref="radarChart" class="chart_box" :style="{ height: reportHeight || `${height}px` }"></div>
   </div>
 </template>
 <script>
@@ -22,6 +22,10 @@ export default {
         type: Number,
         default: 400
     },//高度
+    reportHeight: {
+        type: [Number,String],
+        default: ''
+    },//高度
     showLegend: {
       type: Boolean,
       default: true
@@ -72,6 +76,22 @@ export default {
       ],
       require: true
     },
+    isClick:{
+      type:Boolean,
+      default:false
+    },//是否可以点击事件
+    showRadiusAxis:{//是否显示
+      type:Boolean,
+      default:true
+    },
+    fontSize:{
+      type:[Number,String],
+      default:''
+    },
+    fontColor:{
+      type:String,
+      default:''
+    }
   },
   components: {},
   data() {
@@ -130,16 +150,18 @@ export default {
           name: name,
           max: this.unit == '%' ? 100 : null,
           min: this.unit == '%' ? 0 : null,
-          axisLabel: index ==0 ? {
+          axisLabel: this.showRadiusAxis && index ==0 ? {
             show: true,
             formatter: (val) => `${val}${this.unit}`,
             textStyle: {
-              fontSize: 12,
+              fontSize: this.fontSize?this.fontSize:12,
               color: '#666'
             }
           } : {
             show: false
-          }
+          },
+          color:this.isClick && index == 0?'#2e64fa':(this.fontColor?this.fontColor:'#666666'),
+          nameTextStyle: { fontSize: this.fontSize?this.fontSize:14, fontWeight: this.isClick && index==0?'bold':'normal' }
         };
       });
       console.log("打印indicators数据",indicators);
@@ -263,7 +285,7 @@ export default {
           triggerOn: "mousemove | click",
           renderModel:'html',//使用html渲染模式
           confine:true,//是否将 tooltip 框限制在图表的区域内。
-          extraCssText: 'border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-warp:break-word;max-width: 400px;', // 设置最大宽度和高度
+          extraCssText: 'border-radius: 4px;white-space:normal;word-warp:break-word;max-width: 400px;', // 设置最大宽度和高度
           enterable:true,
           formatter: (params) => {
             const chart = this.myChart.getOption();
@@ -342,15 +364,9 @@ export default {
               color: '#999' // 设置轴线颜色
             }
           },
-
-          name: {
-            textStyle: {
-              fontSize: 14, // 设置字体大小
-              color: '#333'   // 设置字体颜色
-            }
-          },
-          center: this.showLegend?['50%', '55%']:['50%', '55%']//位置调整
+          center: this.reportHeight?['50%', '50%']:(this.showLegend?['50%', '55%']:['50%', '55%'])//位置调整
         },
+        triggerEvent: true,//坐标轴的标签是否响应和触发鼠标事件,默认不响应。
         series: [
           {
             type: "radar",
@@ -397,9 +413,45 @@ export default {
       this.myChart.setOption(option);
       console.log(option,'option')
       this.myChart.on("legendselectchanged", this.handleLegendSelectChanged);
-     
+      // 监听点击事件
+      if(this.isClick){
+        this.bindChartNameEvents(indicators);
+      }
       window.addEventListener("resize", this.resizeFn);
     },
+    //绑定点击事件
+    bindChartNameEvents(indicators){
+      if(!this.myChart){
+        return false
+      }
+      const _that = this;
+      this.myChart.on('click', function(params) {
+        // 判断是否点击的是雷达图指示器(可选:也可去掉判断,点击任何区域都隐藏)
+        if (params.componentType === 'radar' && params.targetType == 'axisName') {
+          let name = params.name;
+          const data = _that.data.slice(1);
+          const  list = data.map(item=>item[0]);
+          const index = list.indexOf(name);
+          let indicatorList = [...indicators];
+          indicatorList.forEach((item,key)=>{
+            if(key==index){
+              item.color = '#2e64fa'
+            }else{
+              item.color = '#666666'
+            }
+            item.nameTextStyle.fontWeight = key==index?'bold':'normal'
+          })
+          // 更新图表配置
+          _that.myChart.setOption({
+            radar: {
+              indicator: indicatorList,
+              
+            }
+          });
+          _that.$emit('HandleChartClick', index,name);//返回点击的柱子索引和x轴名称
+        }
+      });
+    },
     // 图例与显示全部按钮联动 事件
     handleLegendSelectChanged(params) {
       

+ 141 - 0
src/views/analysisReport/personalProfile/Download.vue

@@ -0,0 +1,141 @@
+<template>
+    <el-dialog title="导出精准提升试题" class="mm_dialog" :visible="visible" width="360px" @close="handleClose">
+        <div class="box">
+            <div class="item" :class="{ isAnswer: isAnswer === 1 }" @click="isAnswer = 1">
+                有答案解析
+                <i class="iconfont icon_Check" v-if="isAnswer === 1"></i>
+            </div>
+            <div class="item" :class="{ isAnswer: isAnswer === 0 }" @click="isAnswer = 0">
+                无答案解析
+                <i class="iconfont icon_Check" v-if="isAnswer === 0"></i>
+            </div>
+        </div>
+        <span slot="footer" class="dialog_footer">
+            <el-button @click="handleClose">取 消</el-button>
+            <el-button type="primary" @click="download">下载</el-button>
+        </span>
+    </el-dialog>
+</template>
+
+<script>
+import { ExtractFilename, DownloadFile, CreateLoadingInstance } from './tools'
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        examId: {
+            type: [String, Number],
+            required: true
+        },
+        subjectCode: {
+            type: [String, Number],
+            required: true
+        },
+        knowledgeId: {
+            type: [String, Number],
+            required: true
+        },
+    },
+
+    data() {
+        return {
+            isAnswer: 1
+        };
+    },
+
+    methods: {
+        handleClose() {
+            this.$emit('update:visible', false);
+        },
+
+        download() {
+            let downParams ={
+                examId: this.examId,
+                subjectCode: this.subjectCode,
+                knowledgeId: this.knowledgeId, 
+                isAnswer: this.isAnswer,
+            }
+            const loadingInstance = CreateLoadingInstance()
+            this.$api.personalProfile.downloadPushQuestion(downParams).then(response => {
+                const blob2 = new Blob([response.data], {
+                    type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+                });
+
+                const contentDisposition = response.headers['content-disposition'];
+                const fileName = ExtractFilename(contentDisposition)
+                DownloadFile(blob2, fileName)
+                this.handleClose();
+            }).catch(error => {
+                this.$message.error('下载失败,请稍后重试');
+            }).finally(() => {
+                loadingInstance.close();
+                this.$store.commit("question/SET_VARIANTION", -1); 
+            });
+        }
+    }
+};
+</script>
+
+<style scoped lang="scss">
+.box {
+    display: flex;
+    justify-content: space-between;
+    .item {
+        width: 150px;
+        height: 50px;
+        line-height: 50px;
+        text-align: center;
+        font-size: 14px;
+        color: #909399;
+        background: #EDEFF6;
+        border-radius: 4px;
+        cursor: pointer;
+
+        &.isAnswer {
+            color: #2B65FF;
+            background: rgba(46,100,250,0.1);
+            border: 1px solid #2E64FA;
+            box-shadow: 0 2px 6px rgba(43, 101, 255, 0.12);
+            position: relative;
+            /* 右下角的蓝色三角与对勾 */
+            &::after {
+                content: '';
+                position: absolute;
+                right: 0;
+                bottom: 0;
+                border: 12px solid #2B65FF;
+                border-top-color: transparent;
+                border-left-color: transparent;
+            }
+
+            .iconfont {
+                position: absolute;
+                right: 0;
+                bottom: -18px;
+                font-size: 16px;
+                color: #ffffff;
+                z-index: 1;
+            }
+        }
+    }
+}
+:deep(.el-dialog__headerbtn){
+    top: 13px;
+    font-size: 20px;
+}
+:deep(.el-dialog__title){
+    font-weight: 500;
+    font-size: 16px;
+    color: #303133;
+}
+:deep(.el-button){
+    width: 68px;
+    height: 36px;
+    padding: 0;
+    font-size: 14px;
+    font-weight: 500;
+    margin-left: 20px;
+}
+</style>

+ 520 - 0
src/views/analysisReport/personalProfile/HistoricalChangeChart.vue

@@ -0,0 +1,520 @@
+<template>
+    <div class="historical_change_chart">
+        <div class="chart_title">
+            <span class="secondary_title" style="font-weight: bold;">2、</span>
+            <span class="secondary_title" v-if="titleParts.knowledgeName">{{ titleParts.knowledgeName }} / </span>
+            <span class="main_title">历次变化</span>
+        </div>
+        <div style="position: relative">
+            <el-checkbox @change="(e) => { this.allSeriesSelected = e; this.toggleAllSeries(e); }"
+                v-model="allSeriesSelected" :indeterminate="isIndeterminate" class="checkbox"
+                v-if="hasData">显示全部</el-checkbox>
+            <div ref="chart" class="chart" v-show="hasData"></div>
+            <div v-show="!hasData" class="module_chart no_content_data no_data" element-loading-background="#ffffff">
+                <span>暂无数据</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+
+export default {
+    name: 'HistoricalChangeChart',
+    props: {
+        // 个人得分率
+        personalList: {
+            type: Array,
+            default: () => []
+        },
+        // 班级得分率
+        classList: {
+            type: Array,
+            default: () => []
+        },
+        // 年级得分率
+        gradeList: {
+            type: Array,
+            default: () => []
+        },
+        // 考试名称
+        examName: {
+            type: Array,
+            default: () => []
+        },
+
+        // 知识点名称
+        knowledgeName: {
+            type: String,
+            default: ''
+        },
+    },
+    computed: {
+        // 判断是否有数据
+        hasData() {
+            return this.personalList && this.personalList.length > 0 &&
+                ((this.classList && this.classList.length > 0) ||
+                    (this.gradeList && this.gradeList.length > 0) ||
+                    (this.examName && this.examName.length > 0));
+        },
+
+        // 动态生成标题各部分
+        titleParts() {
+            return {
+                knowledgeName: this.knowledgeName || ''
+            };
+        }
+    },
+    data() {
+        return {
+            chart: null, // echarts实例
+            allSeriesSelected: true, // 显示全部复选框状态
+            isIndeterminate: false, // 不确定状态
+        };
+    },
+    mounted() {
+        // 初始化echarts图表,确保DOM渲染完成
+        this.$nextTick(() => {
+            if (this.hasData) {
+                this.initChart();
+            }
+        });
+
+        // 监听窗口大小变化
+        window.addEventListener('resize', () => {
+            if (this.chart) {
+                this.chart.resize();
+            }
+        });
+    },
+    beforeDestroy() {
+        // 销毁echarts实例
+        if (this.chart) {
+            this.chart.dispose();
+        }
+    },
+    watch: {
+        // 监听hasData变化,动态初始化或销毁图表
+        hasData(newVal) {
+            this.$nextTick(() => {
+                if (newVal) {
+                    this.initChart();
+                } else if (this.chart) {
+                    this.chart.dispose();
+                    this.chart = null;
+                }
+            });
+        },
+        // 监听props变化,动态更新图表数据
+        personalList: {
+            handler(newVal) {
+                if (this.hasData) {
+                    this.initChart();
+                }
+            },
+            deep: true
+        },
+        classList: {
+            handler(newVal) {
+                if (this.hasData) {
+                    this.initChart();
+                }
+            },
+            deep: true
+        },
+        gradeList: {
+            handler(newVal) {
+                if (this.hasData) {
+                    this.initChart();
+                }
+            },
+            deep: true
+        },
+        examName: {
+            handler(newVal) {
+                if (this.hasData) {
+                    this.initChart();
+                }
+            },
+            deep: true
+        },
+    },
+    methods: {
+        // 初始化echarts图表
+        initChart() {
+            // 销毁现有的图表实例
+            if (this.chart) {
+                this.chart.dispose();
+            }
+
+            // 创建echarts实例
+            this.chart = echarts.init(this.$refs.chart);
+
+            // 使用props中的数据
+            const examNames = this.examName; // 考试名称
+            const personalList = this.personalList; ///个人得分率
+            const classList = this.classList; //班级得分率
+            const gradeList = this.gradeList; //年级得分率
+            // 动态生成series数组,只包含有数据的系列
+            const series = [];
+            const legendData = [];
+            // 判断个人得分率数据是否有效(非空且包含有效值)
+            const hasPersonalData = personalList && personalList.length > 0 && personalList.some(data => data !== null && data !== undefined && data !== '');
+            // 判断班级数据是否有效(非空且包含有效值)
+            const hasClassData = classList && classList.length > 0 && classList.some(score => score !== null && score !== undefined && score !== '');
+            // 判断年级数据是否有效(非空且包含有效值)
+            const hasGradeData = gradeList && gradeList.length > 0 && gradeList.some(data => data !== null && data !== undefined && data !== '');
+
+
+            // 如果有个人得分率数据,添加个人得分率系列(使用柱状图)
+            if (hasPersonalData) {
+                // 将personalList转换为数字数组,确保echarts能正确显示
+                const numericPersonalList = personalList.map(val => {
+                    const num = parseFloat(val);
+                    return isNaN(num) ? null : num;
+                });
+                
+                series.push({
+                    name: '个人',
+                    type: 'bar',
+                    data: numericPersonalList,
+                    barWidth: 60,
+                    itemStyle: {
+                        color: '#5470C6'
+                    },
+                    label: {
+                        show: true,
+                        position: 'top',
+                        formatter: '{c}%',
+                        fontSize: 14,
+                        color: '#666666'
+                    }
+                });
+                legendData.push('个人');
+            }
+
+            // 如果有班级数据,添加班级系列(折线图)
+            // if (hasClassData) {
+            //     // 将classList转换为数字数组,确保echarts能正确显示
+            //     const numericClassList = classList.map(val => {
+            //         const num = parseFloat(val);
+            //         return isNaN(num) ? null : num;
+            //     });
+                
+            //     series.push({
+            //         name: '班级',
+            //         type: 'line',
+            //         data: numericClassList,
+            //         itemStyle: {
+            //             color: '#ffffff',
+            //             borderColor: '#3BA272',
+            //             borderWidth: 2 // 边框宽度
+            //         },
+            //         lineStyle: {
+            //             color: '#3BA272',
+            //             width: 2
+            //         },
+            //         // 默认显示圆点
+            //         showSymbol: true,
+            //         symbol: 'circle',
+            //         symbolSize: 10,
+            //         // 鼠标经过时显示圆点
+            //         emphasis: {
+            //             showSymbol: true,
+            //             itemStyle: {
+            //                 color: '#ffffff',
+            //                 borderColor: '#3BA272',
+            //                 borderWidth: 3
+            //             },
+            //             symbolSize: 10
+            //         }
+            //     });
+            //     legendData.push('班级');
+            // }
+
+            // // 如果有年级数据,添加年级系列
+            // if (hasGradeData) {
+            //     // 将gradeList转换为数字数组,确保echarts能正确显示
+            //     const numericGradeList = gradeList.map(val => {
+            //         const num = parseFloat(val);
+            //         return isNaN(num) ? null : num;
+            //     });
+                
+            //     series.push({
+            //         name: '年级',
+            //         type: 'line',
+            //         data: numericGradeList,
+            //         // smooth: true,
+            //         itemStyle: {
+            //             color: '#ffffff',
+            //             borderColor: '#FAC858',
+            //             borderWidth: 2 // 边框宽度
+            //         },
+            //         lineStyle: {
+            //             color: '#FAC858',
+            //             width: 2
+            //         },
+            //         // 默认显示圆点
+            //         showSymbol: true,
+            //         symbol: 'circle',
+            //         symbolSize: 10,
+            //         // 鼠标经过时显示圆点
+            //         emphasis: {
+            //             showSymbol: true,
+            //             itemStyle: {
+            //                 color: '#ffffff',
+            //                 borderColor: '#FAC858',
+            //                 borderWidth: 3,
+            //             },
+            //             symbolSize: 10
+            //         }
+            //     });
+            //     legendData.push('年级');
+            // }
+
+            // 配置项
+            const option = {
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: {
+                        type: 'cross',
+                    },
+                    formatter: function (params) {
+                        if (params.length === 0) return '';
+                        let result = `<span style="font-size:14px; font-weight:bold;">${params[0].name}</span>` + ':<br/>';
+                        params.forEach(item => {
+                            let span = '';
+                            if (item.seriesName === '年级') {
+                                span = `<span         
+                                        style="
+                                        background-color:#FAC858;
+                                        width:15px;
+                                        height:2px;
+                                        display:block;
+                                        float:left;
+                                        border-radius:10px;
+                                        position:absolute;
+                                        top:10px;
+                                        left:0;
+                                        "></span>`;
+                            } else if (item.seriesName === '班级') {
+                                span = `<span 
+                                        style="background-color:#3BA272;
+                                        width:15px;
+                                        height:2px;
+                                        display:block;
+                                        float:left;
+                                        border-radius:10px;
+                                        position:absolute;
+                                        top:10px;
+                                        left:0;
+                                        "></span>`;
+                            } else {
+                                span = `<span 
+                                        style="
+                                        background-color:#5470C6;
+                                        width:10px;
+                                        height:10px;
+                                        display:block;
+                                        float:left;
+                                        position:absolute;
+                                        top:7px;
+                                        left:2px;
+                                        "></span>`;
+                            }
+                            if (item.value !== null && item.value !== undefined && item.value !== '') {
+                                result += `<div style="width:100%; margin:0; padding:0; position:relative; padding-left:20px;">${span} <span style="font-size:12px;">${item.seriesName}得分率: ${item.value}%</span></div>`;
+                            } else {
+                                result += `<div style="width:100%; margin:0; padding:0; position:relative; padding-left:20px;">
+                                    ${span} <span style="font-size:12px;">${item.seriesName}得分率:暂无数据</span>
+                                </div>`;
+                             
+                            }
+                        });
+                        return result;
+                    }
+                },
+                legend: {
+                    data: legendData,
+                    left: 90,
+                    top: -2,
+                    orient: 'horizontal',
+                    itemGap: 20,
+                    itemWidth: 20,
+                    itemHeight: 10,
+                    textStyle: {
+                        fontSize: 12,
+                        color: '#333'
+                    },
+                    selectedMode: 'multiple',
+                    selected: {
+                        // 默认选中所有有数据的系列
+                        ...(hasClassData ? { '班级': true } : {}),
+                        ...(hasPersonalData ? { '个人': true } : {}),
+                        ...(hasGradeData ? { '年级': true } : {})
+                    }
+                },
+                grid: {
+                    left: '0%',
+                    right: '0%',
+                    bottom: '2%',
+                    top: '15%',
+                    containLabel: true
+                },
+                xAxis: {
+                    type: 'category',
+                    data: examNames,
+                    axisLabel: {
+                        fontSize: 14,
+                        rotate: 0,
+                        color: '#666',
+                        formatter: function (value) {
+                            // 名称过长时换行,每4个字符换一行
+                            if (value.length > 10) {
+                                return value.replace(/(.{10})/g, '$1\n');
+                            }
+                            return value;
+                        },
+                        height: 60, // 设置足够的高度来容纳多行标签
+                        verticalAlign: 'top'
+                    }
+                },
+                yAxis: {
+                    type: 'value',
+                    // name: '百分比',
+                    axisLabel: {
+                        formatter: '{value}%',
+                        fontSize: 14,
+                        color: '#666',
+                    },
+                    max: 100
+                },
+                series: series
+            };
+
+            // 设置配置项
+            this.chart.setOption(option);
+
+            // 监听图例点击事件,更新全选状态
+            this.chart.on('legendselectchanged', (params) => {
+                const selected = params.selected;
+                const selectedSeriesCount = Object.values(selected).filter(Boolean).length;
+                const totalSeriesCount = legendData.length;
+
+                // 检查是否所有系列都被选中
+                const isAllSelected = selectedSeriesCount === totalSeriesCount;
+                const isNoneSelected = selectedSeriesCount === 0;
+
+                this.$nextTick(() => {
+                    this.allSeriesSelected = isAllSelected;
+                    this.isIndeterminate = !isAllSelected && !isNoneSelected;
+                });
+            });
+        },
+
+        // 切换所有系列的显示状态
+        toggleAllSeries(showAll) {
+            if (this.chart) {
+                // 动态生成selected对象,只包含有数据的系列
+                const selected = {};
+
+
+                // 判断个人数据是否有效
+                const hasPersonalData = this.personalList && this.personalList.length > 0 && this.personalList.some(data => data !== null && data !== undefined);
+                // 判断班级数据是否有效
+                const hasClassData = this.classList && this.classList.length > 0 && this.classList.some(score => score !== null && score !== undefined && score !== '');
+                // 判断年级数据是否有效
+                const hasGradeData = this.gradeList && this.gradeList.length > 0 && this.gradeList.some(data => data !== null && data !== undefined);
+
+                if (hasPersonalData) selected['个人'] = showAll;
+                if (hasClassData) selected['班级'] = showAll;
+                if (hasGradeData) selected['年级'] = showAll;
+
+
+                this.chart.setOption({
+                    legend: {
+                        selected: selected
+                    }
+                });
+                this.isIndeterminate = false;
+            }
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.historical_change_chart {
+    background: #FFFFFF;
+    border-radius: 10px;
+    padding: 20px;
+    margin-bottom: 10px;
+    height: 316px;
+    display: flex;
+    flex-direction: column;
+
+    .chart_title {
+        font-size: 16px;
+        margin-bottom: 20px;
+
+        .main_title {
+            font-weight: 600;
+            color: #333;
+        }
+
+        .secondary_title {
+            font-weight: normal;
+            color: #999;
+        }
+    }
+
+    .chart {
+        flex: 1;
+        width: 100%;
+        height: calc(100% - 10px);
+        min-height: 290px;
+
+    }
+
+    .radios {
+        width: 10px;
+        height: 10px;
+        border-radius: 50%;
+        display: inline-block;
+    }
+
+    [style^="position: relative"] {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+    }
+
+    .checkbox {
+        position: absolute;
+        left: 0;
+        top: -2px;
+        color: #333;
+
+        // margin-top: 4px;
+        :deep(.el-checkbox__label) {
+            font-size: 12px !important;
+        }
+    }
+
+    .no_data {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 100%;
+        height: 220px;
+        font-size: 14px;
+        color: #999999;
+        border-radius: 4px;
+
+        span {
+            margin-top: 11%;
+        }
+    }
+}
+</style>

+ 133 - 0
src/views/analysisReport/personalProfile/KnowledgeTrack.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="knowledgeTrack">
+    <h3 class="title">历次考试知识点追踪</h3>
+    <!-- 内容区域 -->
+    <div class="map_content" v-if="examRange || knowledgeStats.length > 0">
+      <!-- 考试范围说明 -->
+      <div class="knowledge_stats">
+        <p class="range_text" >
+          <span class="dot"></span>历次考试:<span v-html="examRange" style="color:#333333;font-weight:600;"></span>
+        </p>
+        <p class="stats_text" v-for="(item, index) in knowledgeStats" :key="index">
+          <span class="dot"></span> <span v-html="item"></span>
+        </p>
+      </div>
+    </div>
+
+    <!-- 暂无数据 -->
+    <div class="noData" v-else>
+      <div class="module_chart no_content_data no_data" element-loading-background="#ffffff">
+        <span>暂无数据</span>
+      </div>
+    </div>
+
+  </div>
+</template>
+
+<script>
+export default {
+  name: "KnowledgeTrack",
+  props: {
+    // 考试范围
+    examRange: {
+      type: String,
+      default: ''
+    },
+    // 包含知识点数据
+    knowledgeStats: {
+      type: Array,
+      default: () => []
+    },
+    // 模式:class/grade/student
+    mode: {
+      type: String,
+      default: 'grade'
+    }
+  },
+  data() {
+    return {
+      
+    };
+  },
+  computed: {
+    
+  },
+  mounted() {
+
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.knowledgeTrack {
+  background-color: #ffffff;
+  border-radius: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333333;
+    margin: 0 0 20px 0;
+  }
+  .map_content {
+    background: #F7F7F8;
+    border-radius: 5px 5px 5px 5px;
+    border: 1px solid #EBEEF5;
+    padding: 12px;
+    p{
+      margin-bottom: 5px;
+    }
+    .range_text,.stats_text {
+      font-size: 14px;
+      line-height: 1.8;
+      color: #666;
+      padding-left: 17px;
+      position: relative;
+      text-align: justify;
+
+      .dot {
+        position: absolute;
+        left: 0;
+        top: 8px;
+        display: inline-block;
+        width: 6px;
+        height: 6px;
+        background: #666666;
+        border-radius: 50%;
+      }
+    }
+  }
+
+  //暂无数据
+  .noData {
+    position: relative;
+    height: 220px;
+
+    /* 暂无数据样式 */
+    .no_data {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      font-size: 14px;
+      color: #909399;
+      text-align: center;
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 8px;
+      span{
+        margin-top: 11%;
+      }
+    }
+  }
+}  
+
+</style>

+ 359 - 0
src/views/analysisReport/personalProfile/MyGradeHistory.vue

@@ -0,0 +1,359 @@
+<template>
+  <div class="myGradeHistory">
+    <div class="chart_title">
+      <h3 class="title">我的历次成绩</h3>
+      <!-- <el-button type="primary" size="small" class="compare_btn" @click="showExamCompareDialog">
+        <img src="../../../assets/studentAnalysis/echarts.svg" alt="图表" class="btn_icon">
+        历次考试对比
+      </el-button> -->
+    </div>
+
+    <div class="content" v-if="gradeHistoryData.length > 0">
+      <el-table ref="gradeTable" :data="tableData" style="width: 100%" border :header-cell-style="headerCellStyle"
+        :cell-style="cellStyle" stripe @selection-change="handleSelectionChange" width="55" align="center">
+        <el-table-column type="selection" width="55" align="center"></el-table-column>
+        <el-table-column type="index" label="序号" width="70" align="center">
+          <template slot-scope="scope">
+            {{scope.$index <10 ? '0' + (scope.$index + 1) : scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="examName" label="考试名称" align="center"></el-table-column>
+        <el-table-column prop="fullScore" label="满分" width="90" align="center"></el-table-column>
+        <el-table-column prop="rawScore" label="原始分" width="90" align="center"></el-table-column>
+        <el-table-column prop="classRank" label="班级排名" width="90" align="center"></el-table-column>
+        <el-table-column prop="gradeRank" label="年级排名" width="90" align="center"></el-table-column>
+        <el-table-column prop="standardScore" label="标准分" width="90" align="center"></el-table-column>
+        <el-table-column prop="scoreRate" label="得分率" width="90" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.scoreRate }}%
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" align="center">
+          <template slot-scope="scope">
+            <el-button type="text" @click="viewDetails(scope.row)">查看答题卡</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+    </div>
+
+    <!-- 暂无数据 -->
+    <div class="noData" v-else>
+      <div class="module_chart no_content_data no_data" element-loading-background="#ffffff">
+        <span>暂无数据</span>
+      </div>
+    </div>
+
+    <!-- 学生答题卡预览组件 -->
+    <StudentPaper v-model="showStudentPaperDialog" :paperInfo="paperInfo" :currentPageIndex="currentPageIndex"
+      :pageTitle="paperTitle"></StudentPaper>
+
+    <!-- 历次考试对比弹窗 - 使用公共组件 -->
+    <!-- <BenchTaskSelect v-if="showBenchTaskSelect" :showDialog.sync="showBenchTaskSelect" :isMultiple="isMultiple"
+      :title="benchTaskTitle" :before="1" @GetSelectId="GetSelectId"></BenchTaskSelect> -->
+  </div>
+</template>
+
+<script>
+import StudentPaper from '@/components/StudentPaper.vue' //学生答题卡预览组件
+import { mapGetters } from 'vuex'
+// 导入公共组件
+//import BenchTaskSelect from '@/views/analysisReport/components/benchTaskSelectSchool';
+
+export default {
+  components: {
+    StudentPaper,
+    // BenchTaskSelect
+  },
+  name: "MyGradeHistory",
+  props: {
+    mode: {
+      type: String,
+      default: 'grade'
+    },
+    // 接收历次成绩数据
+    gradeHistoryData: {
+      type: Array,
+      default: () => []
+    },
+    // 接收个人画像数据
+    portraitData: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  data() {
+    return {
+      showStudentPaperDialog: false, //显示答题卡弹框
+      paperInfo: {},
+      paperTitle: '', //答题卡弹框标题
+      currentPageIndex: 0 ,//当前选中的答题卡第几页
+      // 控制弹窗显示/隐藏
+      showBenchTaskSelect: false,
+      // 是否允许多选
+      isMultiple: true,
+      // 弹窗标题
+      benchTaskTitle: '历次考试对比',
+      // 是否正在初始化全选
+      isInitializing: false,
+    };
+  },
+  watch: {
+    // 监听gradeHistoryData变化,数据加载完成后默认全选
+    gradeHistoryData: {
+      handler(newVal) {
+        if (newVal && newVal.length > 0) {
+          this.$nextTick(() => {
+            if (this.$refs.gradeTable) {
+              // 设置初始化标志,避免触发selection-change事件
+              this.isInitializing = true;
+              // 先清空所有选择,再重新全选
+              this.$refs.gradeTable.clearSelection();
+              // 遍历表格数据,手动选中每一行
+              this.tableData.forEach(row => {
+                this.$refs.gradeTable.toggleRowSelection(row, true);
+              });
+              // 完成后重置标志
+              this.isInitializing = false;
+            }
+          });
+        }
+      },
+      immediate: true
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo']),
+    pageName() {
+      return this.$store.state.report.examSelectItem.examName
+    }, //考试名称
+    reportParam() {
+      return {
+        examLevel: this.$store.state.report.filterObject.examLevel, //1-联考 2-单校
+        contrastExamIds: this.$store.state.report.filterObject.contrastExamIds, //多次考试任务对比ID,不包含当前任务ID
+        examId: this.$store.state.report.filterObject.examId, //考试id
+        subjectCode: this.$store.state.report.filterObject.subjectCode, //科目code
+        subjectGroupType: this.$store.state.report.filterObject.subjectGroupType, //是否为组合科目 1为组合科目 0为非组合科目
+        isTotal: this.$store.state.report.filterObject.isTotal //是否为总分科目 1为总分 0为非总分
+      }
+    }, //分析报告公共参数变量
+    // 处理数据,映射接口字段到表格字段
+    tableData() {
+      return this.gradeHistoryData.map(item => ({
+        id: item.id || Math.random(),
+        examName: item.examName || '',
+        fullScore: item.fullScore,
+        rawScore: item.originalScore, // 接口字段originalScore映射到rawScore
+        classRank: item.classRank,
+        gradeRank: item.gradeRank,
+        standardScore: item.standardScore,
+        scoreRate: item.scoreRate,
+        examId: item.examId, //考试id
+        subjectId: item.subjectId, //科目id
+      }));
+    },
+    // 表头样式
+    headerCellStyle() {
+      return {
+        backgroundColor: '#F5F7FA',
+        color: '#303133',
+        height: '50px',
+        padding: '0',
+        margin: '0',
+        lineHeight: '50px'
+      };
+    },
+    // 表格内容样式
+    cellStyle() {
+      return {
+        color: '#606266',
+        height: '50px',
+        padding: '0',
+        margin: '0',
+        lineHeight: '50px'
+      };
+    },
+  },
+  methods: {
+    // 处理选择变化
+    handleSelectionChange(selection) {
+      // 初始化过程中不触发事件,避免重复调用接口
+      if (this.isInitializing) {
+        return;
+      }
+      let selectedExamIds = selection.map(item => item.examId);
+      // 传递选中的考试ID给父组件
+      this.$emit('selection-change', selectedExamIds);
+    },
+    // 历史考试对比弹窗
+    showExamCompareDialog() {
+      this.showBenchTaskSelect = true;
+    },
+    // 获取选中的ID
+    GetSelectId() {
+      // 获取选中的考试ID
+      const selectedExamIds = this.$store.state.report.lastExamSelectIds || [];
+      // 通过事件将选中的考试ID传递给父组件
+      this.$emit('select-exam-ids', selectedExamIds);
+      // 关闭弹窗
+      this.showBenchTaskSelect = false;
+    },
+    viewDetails(row) {
+      this.paperTitle = `${row.examName}_${this.userInfo.userName}`
+      this.paperInfo = { examId: this.reportParam.examId, subjectCode: this.reportParam.subjectCode }
+      this.showStudentPaperDialog = true
+      // 实际项目中可以跳转到详情页面或展开详情
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.myGradeHistory {
+  background-color: #ffffff;
+  border-radius: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+
+  .chart_title {
+    .title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333333;
+      margin: 0 0 20px 0;
+    }
+
+    // 历史考试对比
+    .compare_btn {
+      font-size: 14px;
+      padding: 8px 10px;
+      background: #FFFFFF;
+      border-radius: 4px;
+      border: 1px solid #DCDFE6;
+      color: #333333;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      .btn_icon {
+        width: 16px;
+        height: 16px;
+        margin-right: 4px;
+        vertical-align: middle;
+      }
+
+      span {
+        vertical-align: middle;
+      }
+    }
+  }
+
+
+  /* 表格区域 */
+  .content {
+
+    /* 调整表格斑马条纹颜色 */
+    :deep(.el-table--striped .el-table__body tr.el-table__row--striped) {
+      background-color: #FAFAFA !important;
+    }
+
+    /* 确保表头行高 */
+    :deep(.el-table__header tr) {
+      height: 50px !important;
+    }
+
+    /* 确保表格内容行高 */
+    :deep(.el-table__body tr) {
+      height: 50px !important;
+    }
+
+    /* 移除表头单元格内外边距 */
+    :deep(.el-table__header th.el-table__cell) {
+      padding: 0 !important;
+      margin: 0 !important;
+      height: 50px !important;
+      line-height: 50px !important;
+    }
+
+    /* 移除表格内容单元格内外边距 */
+    :deep(.el-table__body td.el-table__cell) {
+      padding: 0 !important;
+      margin: 0 !important;
+      height: 50px !important;
+      line-height: 50px !important;
+    }
+
+    /* 表格圆角样式 */
+    :deep(.el-table) {
+      border-radius: 6px !important;
+      overflow: hidden;
+    }
+
+    /* 表头圆角 */
+    :deep(.el-table__header-wrapper) {
+      border-radius: 6px 6px 0 0;
+      overflow: hidden;
+    }
+
+    /* 表尾圆角 */
+    :deep(.el-table__body-wrapper) {
+      border-radius: 0 0 6px 6px;
+      overflow: hidden;
+    }
+
+    /* 表头首列圆角 */
+    :deep(.el-table__header th.el-table__cell:first-child) {
+      border-radius: 6px 0 0 0 !important;
+    }
+
+    /* 表头末列圆角 */
+    :deep(.el-table__header th.el-table__cell:last-child) {
+      border-radius: 0 6px 0 0 !important;
+    }
+
+    /* 表尾首列圆角 */
+    :deep(.el-table__body tr:last-child td.el-table__cell:first-child) {
+      border-radius: 0 0 0 6px !important;
+    }
+
+    /* 表尾末列圆角 */
+    :deep(.el-table__body tr:last-child td.el-table__cell:last-child) {
+      border-radius: 0 0 6px 0 !important;
+    }
+
+    .pagination {
+      margin-top: 16px;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+    }
+  }
+
+  // 暂无数据样式
+  .noData {
+    position: relative;
+    height: 220px;
+
+    /* 暂无数据样式 */
+    .no_data {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      font-size: 14px;
+      color: #909399;
+      text-align: center;
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 8px;
+
+      span {
+        margin-top: 11%;
+      }
+    }
+  }
+}
+</style>

+ 743 - 0
src/views/analysisReport/personalProfile/index.vue

@@ -0,0 +1,743 @@
+<template>
+    <div class="personalProfile">
+        <!-- 我的历次成绩子组件 -->
+        <MyGradeHistory :grade-history-data="gradeHistoryData" v-loading="historyloading" @selection-change="handleSelectionChange" />
+
+        <!-- 历次考试知识点追踪 -->
+        <KnowledgeTrack 
+            :exam-range="knowledgeMapData.examRange" 
+            :knowledge-stats="knowledgeStats" 
+            v-loading="knowledgeloading"
+        />
+
+        <!-- 零分知识点、高频错题知识点 -->
+        <zeroScoreKnowledge 
+            ref="knowledgeGraphRef" 
+            @knowledge-item-click="handleKnowledgeItemClick"
+            :active-view="activeView" 
+            @view-change="activeView = $event" 
+            :subject-name="portraitData.subjectName"
+            :subject-score-rate="subjectScoreRate" 
+            :table-data="treeData" 
+            :fatal-vulnerability="fatalVulnerability"
+            :high-vulnerability="highVulnerability" 
+            :all-knowledge-list="allKnowledgeList"
+            @activeTabChange="handleActiveTabChange" 
+            @legend-change="handleLegendChange"
+            v-loading="zeroloading"
+        />
+
+        <!-- 历次变化 -->
+        <HistoricalChangeChart 
+            ref="historicalChangeChartRef"
+            :personalList="historicalChangeData.personalList"
+            :classList="historicalChangeData.classList" 
+            :gradeList="historicalChangeData.gradeList"
+            :examName="historicalChangeData.examName" 
+            :knowledgeName="knowledgeName" 
+            v-loading="historicalChangeLoading"
+        />
+        <!-- 薄弱知识点精准提升 -->
+         <knowledgePaps
+            :knowledgeName="knowledgeName"
+            v-loading="knowledgePapsLoading"
+            :knowledgData="knowledgData"
+            @export-knowledge-paps="exportKnowledgePaps"
+        />
+        <!-- 下载试题 -->
+        <Download 
+            ref="downloadRef" 
+            :visible.sync="visible"
+            :examId="portraitData.examId" 
+            :subjectCode="portraitData.subjectCode" 
+            :knowledgeId="knowledgeId" 
+        />
+        
+    </div>
+</template>
+<script>
+import MyGradeHistory from './MyGradeHistory.vue'; //我的历次成绩子组件 
+import KnowledgeTrack from './KnowledgeTrack.vue'; //历次考试知识点追踪子组件
+import zeroScoreKnowledge from './zeroScoreKnowledge.vue'; //零分知识点、高频错题知识点子组件
+import HistoricalChangeChart from './HistoricalChangeChart.vue'; //历次变化子组件
+import knowledgePaps from './knowledgePaps.vue'; //薄弱知识点精准提升子组件
+import Download from './Download.vue'; //下载组件
+
+export default {
+    name: "PersonalProfile",
+    components: {
+        MyGradeHistory,
+        KnowledgeTrack,
+        zeroScoreKnowledge,
+        HistoricalChangeChart,
+        knowledgePaps,
+        Download
+    },
+    computed: {
+        portraitData() {
+            return this.$store.state.report.filterObject;
+        },
+        // 动态生成知识统计文本
+        knowledgeStats() {
+            const stats = [];
+            const prefix = ''; // 定义prefix变量
+            
+            // 当有知识点数据时,生成统计文本
+            if (this.knowledgeMapData.konwLenght > 0 || this.knowledgeMapData.knowledgeList) {
+                // 获取得分率变化的文本部分
+                let changeText = '';
+                const hasRaise = this.knowledgeMapData.raiseKnowledgeList; // 提升部分
+                const hasDrop = this.knowledgeMapData.dropKnowledgeList;   // 下降部分
+                const hasKnowledgeList = this.knowledgeMapData.knowledgeList; // 知识点列表
+                const knowledgeNum = this.knowledgeMapData.konwLenght; // 知识点数量
+                
+                // 基础文本:包含的知识点数量
+                if (knowledgeNum > 0) {
+                    changeText += `共包含<span style='color:#2E64FA'>${knowledgeNum}</span>个知识点`;
+                }
+                
+                // 添加知识点列表(可选)
+                if (hasKnowledgeList) {
+                    changeText += `,包括<span style="color:#333333;font-weight:600;">${this.knowledgeMapData.knowledgeList}</span>`;
+                }
+                
+                // 当repeatKnowledgeNum大于0时,添加得分率变化信息
+                if (this.knowledgeMapData.repeatKnowledgeNum > 0) {
+                    // 只有当raiseKnowledgeList不为空时,添加得分率提升的部分
+                    if (hasRaise) {
+                        changeText += `,其中<span style="color:#333333;font-weight:600;">${hasRaise}</span>${prefix}得分率<span style="color:#3BA272;font-weight:600;">提升</span>`;
+                    }
+                    // 当下降部分不为空时,添加得分率下降的部分
+                    if (hasDrop) {
+                        // 当提升部分不为空时,添加逗号
+                        if (hasRaise) {
+                            changeText += `,`;
+                        } else {
+                            changeText += `,其中`;
+                        }
+                        changeText += `<span style="color:#333333;font-weight:600;">${hasDrop}</span>${prefix}得分率<span style="color:#F56C6C;font-weight:600;">下降</span>,希望针对下降的知识点进行总结和反思。`;
+                    } else if (!hasRaise) {
+                        // 如果没有提升和下降,添加完整句号
+                        changeText += `。`;
+                    } else {
+                        // 如果只有提升,添加句号
+                        changeText += `。`;
+                    }
+                } else {
+                    // 如果repeatKnowledgeNum为0,添加完整句号
+                    changeText += `。`;
+                }
+                
+                // 确保始终返回数组
+                if (changeText) {
+                    stats.push(changeText);
+                }
+            } else if (this.knowledgeMapData.knowledgeList) {
+                // 如果只有考试范围数据,添加说明文本
+                stats.push(`本次考试涵盖<span style="color:#333333;font-weight:600;">${this.knowledgeMapData.knowledgeList}</span>,暂无详细知识点追踪数据。`);
+            }
+            
+            return stats;
+        },
+
+    },
+    data() {
+        return {
+            // 历次成绩数据
+            gradeHistoryData: [],
+            // 历次考试知识点追踪数据
+            knowledgeMapData: {
+                examRange: "", //考试名称
+                konwLenght: 0, // 包含知识点数
+                knowledgeList: [],// 包含知识点列表 
+                repeatKnowledgeNum: 0, // 重复知识点数
+                raiseKnowledgeList: [], //提升知识点
+                dropKnowledgeList: [],//下降知识点 
+            },
+            // 零点知识点数据
+            activeView: 'graph', // 默认图形视图
+            // 按图形查看
+            subjectScoreRate: 0, // 科目得分率
+            // 知识点树状图数据
+            treeData: [],
+            // 零分知识点数据
+            fatalVulnerability: [],
+            // 高频错题知识点数据
+            highVulnerability: [],
+            // 所有知识点数据
+            allKnowledgeList: [],
+            // 知识点名称
+            knowledgeName: '',
+            knowledgeId: '',
+            // 历次变化图表数据
+            historicalChangeData: {
+                personalList: [], //个人得分率
+                classList: [], //班级得分率
+                gradeList: [],//年级得分率
+                examName: [] // 考试名称
+            },
+            // 薄弱知识点数据
+            knowledgData: [],
+            // 我的历次成绩-加载状态
+            historyloading: false,
+            // 历次考试知识点追踪-加载状态
+            knowledgeloading: false,
+            // 零分知识点、高频错题知识点-加载状态
+            zeroloading: false,
+            // 历次变化图表-加载状态
+            historicalChangeLoading: false,
+            // 薄弱知识点精准提升-加载状态
+            knowledgePapsLoading: false,
+            // 下载组件显示状态
+            visible: false,
+        };
+    },
+    watch: {
+        // 监听knowledgeId变化
+        portraitData(newVal, oldVal) {
+            if (newVal !== oldVal) {
+                // 点击知识点后,更新历次变化
+                this.MyGradeHistoryData();
+            }
+        }
+    },
+    created() {
+        // 我的历次成绩-并且获取历次考试ids examIds用于调用其余接口
+        this.MyGradeHistoryData();
+    },
+    methods: {
+        // 我的历次成绩---年级画像
+        MyGradeHistoryData() {
+            // 加载状态-清空数据
+            this.historyloading = true;
+            // 历次考试知识点追踪-加载状态
+            this.knowledgeloading = true;
+            // 零分知识点、高频错题知识点-加载状态
+            this.zeroloading = true;
+             // 薄弱知识点精准提升-加载状态
+             this.knowledgePapsLoading = true;
+            // 历次变化图表-加载状态
+            this.historicalChangeLoading = true;
+            this.gradeHistoryData = [];
+            let examParams = {
+                examId: this.portraitData.examId,
+                subjectCode: this.portraitData.subjectCode,
+            };
+            this.$api.personalProfile.examScoreList(examParams).then(res => {
+                if (res.code === 200) {
+                    let data = res.data || [];
+                    if (data) {
+                        this.gradeHistoryData = res.data.records || [];
+                        let examIds = this.gradeHistoryData.map(item => item.examId || '');
+                        this.portraitData.examIds = examIds;
+                        // 加载状态
+                        this.historyloading = false;
+
+                        // 零分知识点、高频错题知识点
+                        this.vulerabData();
+                        // 历次考试知识点追踪
+                        this.previousExamsData();
+                    }else{
+                        // 加载状态
+                        // 历次考试知识点追踪-加载状态
+                        this.knowledgeloading = false;
+                        // 零分知识点、高频错题知识点-加载状态
+                        this.zeroloading = false;
+                        // 薄弱知识点精准提升-加载状态
+                        this.knowledgePapsLoading = false;
+                        // 历次变化图表-加载状态
+                        this.historicalChangeLoading = false;    
+                        this.historyloading = false;
+                        this.gradeHistoryData = [];
+                    }
+                }else{
+                    // 加载状态
+                    this.historyloading = false;
+                    this.gradeHistoryData = [];
+                }
+            })
+        },
+        // 处理选择变化
+        handleSelectionChange(selectedExamIds) {
+            this.portraitData.examIds = selectedExamIds;
+            // 历次考试知识点追踪
+            this.previousExamsData();
+            // 零分知识点、高频错题知识点(会自动调用 KnowledgeTrackData 和 pushQuestionData)
+            this.vulerabData();
+        },
+        //历次考试知识点追踪
+        previousExamsData() {
+            // 加载状态-清空数据
+            this.knowledgeloading = true;
+            this.knowledgeMapData = {
+                examRange: "", //考试名称
+                konwLenght: 0, // 包含知识点数
+                knowledgeList: [],// 包含知识点列表 
+                repeatKnowledgeNum: 0, // 重复知识点数
+                raiseKnowledgeList: [], //提升知识点
+                dropKnowledgeList: [],//下降知识点 
+            };
+            let examParams = {
+                examId: this.portraitData.examId, //当前考试id
+                subjectCode: this.portraitData.subjectCode, //学科code
+                examIds: this.portraitData.examIds, //历次考试ids
+            };
+            this.$api.personalProfile.examKnowledgePointTrack(examParams).then(res => {
+                if (res.code === 200) {
+                    let data = res.data;
+                    if (data) {
+                        this.knowledgeloading = false;
+                        // 考试名称列表
+                        if (data.examNameList) {
+                            this.knowledgeMapData.examRange = data.examNameList.join('、') || '';
+                        }
+                        // 知识点列表
+                        if (data.knowledgeList) {
+                            this.knowledgeMapData.konwLenght = data.knowledgeList.length || 0; //知识点列表长度
+                            this.knowledgeMapData.knowledgeList = data.knowledgeList.join('、') || '';
+                        }
+                        this.knowledgeMapData.repeatKnowledgeNum = data.repeatKnowledgeNum || 0; //重复知识点数
+                        // 提升知识点
+                        if (data.raiseKnowledgeList) {
+                            this.knowledgeMapData.raiseKnowledgeList = data.raiseKnowledgeList.join('、') || '';
+                        }
+                        // 下降知识点
+                        if (data.dropKnowledgeList) {
+                            this.knowledgeMapData.dropKnowledgeList = data.dropKnowledgeList.join('、') || '';
+                        }
+                    }else{
+                        // 加载状态
+                        this.knowledgeloading = false;
+                        this.knowledgeMapData = {
+                            examRange: "", //考试名称
+                            konwLenght: 0, // 包含知识点数
+                            knowledgeList: [],// 包含知识点列表 
+                            repeatKnowledgeNum: 0, // 重复知识点数
+                            raiseKnowledgeList: [], //提升知识点
+                            dropKnowledgeList: [],//下降知识点 
+                        };
+                    }
+                }else{
+                    // 加载状态
+                    this.knowledgeloading = false;
+                    this.knowledgeMapData = {
+                        examRange: "", //考试名称
+                        konwLenght: 0, // 包含知识点数
+                        knowledgeList: [],// 包含知识点列表 
+                        repeatKnowledgeNum: 0, // 重复知识点数
+                        raiseKnowledgeList: [], //提升知识点
+                        dropKnowledgeList: [],//下降知识点 
+                    };
+                }
+            })
+        },
+
+        // 零分知识点、高频错题知识点----年级
+        vulerabData() {
+            // 加载状态-清空数据
+            this.zeroloading = true;
+            this.treeData = [];
+            this.subjectScoreRate = 0; //科目得分率
+            this.fatalVulnerability = []; // 零分知识点数据
+            this.highVulnerability = []; // 高频错题知识点数据
+            this.allKnowledgeList = []; // 所有知识点数据
+            let examParams = {
+                examId: this.portraitData.examId, //当前考试id
+                examIds: this.portraitData.examIds, //历次考试ids
+                subjectCode: this.portraitData.subjectCode, //学科code
+                knowledgeId: this.portraitData.knowledgeId, //	知识点id(针对历次和试题查询)
+            };
+            this.$api.personalProfile.studentKnowledgeGraph(examParams).then(res => {
+                if (res.code === 200) {
+                    let data = res.data;
+                    if (data) {
+                        // 加载状态-清空数据
+                        this.zeroloading = false;
+
+                        // 将字符串转换为数字类型
+                        this.subjectScoreRate = parseFloat(data.subjectScoreRate) || 0; //科目得分率
+                        // 零分知识点数据
+                        this.fatalVulnerability = data.fatalVulnerability || [];
+                        // 高频错题知识点数据
+                        this.highVulnerability = data.highVulnerability || [];
+                        // 所有知识点数据
+                        this.allKnowledgeList = data.allKnowledgeList || [];
+
+                        // 新增:调用studentKnowledgeDataTree API获取treeData
+                        this.getStudentKnowledgeDataTree();
+                        
+                        // 获取知识点第一条数据id 全部>高频错题>零分知识点
+                        if (this.allKnowledgeList.length > 0) {
+                            this.knowledgeName = this.allKnowledgeList[0].knowledgeName || '';
+                            this.knowledgeId = this.allKnowledgeList[0].knowledgeId || 0;
+                        } else {
+                            if (this.highVulnerability.length > 0) {
+                                this.knowledgeName = this.highVulnerability[0].knowledgeName || '';
+                                this.knowledgeId = this.highVulnerability[0].knowledgeId || 0;
+                            }else if (this.fatalVulnerability.length > 0) {
+                                this.knowledgeName = this.fatalVulnerability[0].knowledgeName || '';
+                                this.knowledgeId = this.fatalVulnerability[0].knowledgeId || 0;
+                            } else {
+                                this.knowledgeName = '';
+                                this.knowledgeId = '';
+                            }
+                        }
+                        
+                        // 首次加载默认获取知识点
+                        this.KnowledgeTrackData();
+                        // 加载推送试题
+                        this.pushQuestionData();
+                    }else{
+                        this.treeData = [];
+                        this.subjectScoreRate = 0; //科目得分率
+                        this.fatalVulnerability = []; // 零分知识点数据
+                        this.highVulnerability = []; // 高频错题知识点数据
+                        this.allKnowledgeList = []; // 所有知识点数据
+                        // 首次加载默认获取知识点
+                        this.zeroloading = false;
+                        this.historicalChangeLoading = false;
+                        this.knowledgePapsLoading = false;
+                        // 清除历次变化图表数据
+                        this.historicalChangeData = {
+                            personalList: [],
+                            classList: [],
+                            gradeList: [],
+                            examName: []
+                        };
+                        // 接口调用成功后,刷新图表
+                        this.$nextTick(() => {
+                            if (this.$refs.knowledgeGraphRef) {
+                                this.$refs.knowledgeGraphRef.updateChart();
+                            }
+                        });
+                    }
+                }else{
+                    this.treeData = [];
+                    this.subjectScoreRate = 0; //科目得分率
+                    this.fatalVulnerability = []; // 零分知识点数据
+                    this.highVulnerability = []; // 高频错题知识点数据
+                    this.allKnowledgeList = []; // 所有知识点数据
+                    // 加载状态
+                    this.zeroloading = false;
+                    // 接口调用成功后,刷新图表
+                    this.$nextTick(() => {
+                        if (this.$refs.knowledgeGraphRef) {
+                            this.$refs.knowledgeGraphRef.updateChart();
+                        }
+                    });
+                }
+            })
+        },
+        
+        // 新增:获取知识点树状图数据
+        getStudentKnowledgeDataTree(knowledgeType = 0, scoreRateTypes = null) {
+            this.treeData = [];
+            let params = {
+                examId: this.portraitData.examId, //当前考试id
+                examIds: this.portraitData.examIds, //历次考试ids
+                subjectCode: this.portraitData.subjectCode, //学科code
+                knowledgeId: this.portraitData.knowledgeId, //知识点id(针对历次和试题查询)
+                scoreRateTypes: scoreRateTypes, // 得分率类型 1薄弱 2良好 3优秀(精准提升),为空是全部
+                knowledgeType: knowledgeType // 知识点类型 0全部 1高频错题知识点 2零分知识点(精准提升)
+            };
+            
+            this.$api.personalProfile.studentKnowledgeDataTree(params).then(res => {
+                if (res.code === 200) {
+                    let data = res.data;
+                    if (data) {
+                        // 从新API获取treeData
+                        this.treeData = data || [];
+                        
+                        // 接口调用成功后,刷新图表
+                        this.$nextTick(() => {
+                            if (this.$refs.knowledgeGraphRef) {
+                                this.$refs.knowledgeGraphRef.updateChart();
+                            }
+                        });
+                    } else {
+                        this.treeData = [];
+                        // 接口调用成功后,刷新图表
+                        this.$nextTick(() => {
+                            if (this.$refs.knowledgeGraphRef) {
+                                this.$refs.knowledgeGraphRef.updateChart();
+                            }
+                        });
+                    }
+                } else {
+                    this.treeData = [];
+                    // 接口调用成功后,刷新图表
+                    this.$nextTick(() => {
+                        if (this.$refs.knowledgeGraphRef) {
+                            this.$refs.knowledgeGraphRef.updateChart();
+                        }
+                    });
+                }
+            });
+        },
+        
+        // 处理图例变化事件
+        handleLegendChange(data) {
+            // 调用getStudentKnowledgeDataTree方法,传递knowledgeType和scoreRateTypes
+            this.getStudentKnowledgeDataTree(data.knowledgeType, data.scoreRateTypes);
+        },
+
+        // 获取历次变化数据
+        KnowledgeTrackData() {
+            // 加载状态-清空数据
+            this.historicalChangeLoading = false;
+            // 清除历次变化图表数据
+            this.historicalChangeData = {
+                personalList: [],
+                classList: [],
+                gradeList: [],
+                examName: []
+            };
+            let params = {
+                examId: this.portraitData.examId, //当前考试id
+                examIds: this.portraitData.examIds, //历次考试ids
+                subjectCode: this.portraitData.subjectCode, //学科code
+                knowledgeId: this.knowledgeId, //	知识点id(针对历次和试题查询)
+            };
+            this.$api.personalProfile.knowledgeExamPrevious(params).then(res => {
+                if (res.code === 200) {
+                    let data = res.data
+                    if (!data) {
+                        this.$message.error(res.msg);
+                        return
+                    }
+                    // 加载状态-清空数据
+                    this.historicalChangeLoading = false;
+
+                    // 初始化空数组
+                    let personalList = []; //个人得分率
+                    let classList = []; //班级得分率
+                    let gradeList = []; //年级得分率
+                    let examNames = []; //考试名称
+
+                    // 遍历数据
+                    if (data && Array.isArray(data)) {
+                        data.forEach(item => {
+                            // 添加个人得分率
+                            personalList.push(item.personalScoreRate);
+                            // 添加班级得分率
+                            classList.push(item.classScoreRate);
+                            // 添加年级得分率
+                            gradeList.push(item.gradeScoreRate);
+                            // 添加考试名称
+                            examNames.push(item.examName);
+                        });
+                    }
+                    // 更新历次变化图表数据
+                    this.historicalChangeData = {
+                        personalList: personalList,
+                        classList: classList,
+                        gradeList: gradeList,
+                        examName: examNames,
+                    };
+                    
+                    this.$nextTick(() => {
+                        if (this.$refs.historicalChangeChartRef) {
+                            this.$refs.historicalChangeChartRef.initChart();
+                        }
+                    });
+                } else {
+                    // 清除历次变化图表数据
+                    this.historicalChangeData = {
+                        personalList: [],
+                        classList: [],
+                        gradeList: [],
+                        examName: []
+                    };
+                    // 加载状态-清空数据
+                    this.historicalChangeLoading = false;
+                }
+            }).catch(err => {
+                // 清除历次变化图表数据
+                this.historicalChangeData = {
+                    personalList: [],
+                    classList: [],
+                    gradeList: [],
+                    examName: []
+                };
+                // 加载状态-清空数据
+                this.historicalChangeLoading = false;
+            })
+        },
+
+        // 推题列表
+        pushQuestionData(){
+            this.knowledgData = [];
+            let params = {
+                examId: this.portraitData.examId, //当前考试id
+                subjectCode: this.portraitData.subjectCode, //学科code
+                knowledgeId: this.knowledgeId, //知识点id(针对历次和试题查询)
+            };
+            this.$api.personalProfile.pushQuestionList(params).then(res => {
+                if (res.code === 200) {
+                    let data = res.data ;
+                    if (data) {
+                        // 加载状态-清空数据
+                        this.knowledgePapsLoading = false;
+                         // 更新推题列表数据
+                        this.knowledgData = data || [];
+                    }else{
+                        // 加载状态-清空数据
+                        this.knowledgePapsLoading = false;
+                         // 更新推题列表数据
+                        this.knowledgData =  [];
+                    }
+                } else {
+                   // 加载状态-清空数据
+                    this.knowledgePapsLoading = false;
+                    // 更新推题列表数据
+                    this.knowledgData =  [];
+                }
+            });
+        },
+
+        // 知识点列表点击事件
+        handleKnowledgeItemClick(data) {
+            // 点击知识点后,更新图表数据
+            this.knowledgeId = data.item.knowledgeId ;
+            this.knowledgeName = data.item.knowledgeName ;
+            // 点击知识点后,更新历次变化
+            this.KnowledgeTrackData();
+            // 点击知识点后,更新推题列表
+            this.pushQuestionData();
+        },
+
+        // 零分、高频、全部知识点切换
+        handleActiveTabChange(tab) {
+            // highFreq 高频
+            // zero 零分
+            // all 所有知识点
+            let knowledgeType = 0;
+            
+            if (tab === 'highFreq') {
+                // 高频知识点
+                knowledgeType = 1;
+                if(this.highVulnerability.length > 0) {
+                    this.knowledgeId = this.highVulnerability[0].knowledgeId ;
+                    this.knowledgeName = this.highVulnerability[0].knowledgeName;
+                } else {
+                    this.knowledgeId = '';
+                    this.knowledgeName = '';
+                }
+            } else if (tab === 'zero') {
+                // 零分知识点
+                knowledgeType = 2;
+                if(this.fatalVulnerability.length > 0) {
+                    this.knowledgeId = this.fatalVulnerability[0].knowledgeId || 0;
+                    this.knowledgeName = this.fatalVulnerability[0].knowledgeName || '';
+                } else {
+                    this.knowledgeId = '';
+                    this.knowledgeName = '';
+                }
+            } else if (tab === 'all') {
+                // 所有知识点
+                knowledgeType = 0;
+                if(this.allKnowledgeList.length > 0) {
+                    this.knowledgeId = this.allKnowledgeList[0].knowledgeId || 0;
+                    this.knowledgeName = this.allKnowledgeList[0].knowledgeName || '';
+                } else {
+                    this.knowledgeId = '';
+                    this.knowledgeName = '';
+                }
+            }
+            
+            // 重置图例状态:让三个图例恢复成原先的模样
+            if (this.$refs.knowledgeGraphRef) {
+                this.$refs.knowledgeGraphRef.selectedLegend = {
+                    weak: true,
+                    good: true,
+                    excellent: true
+                };
+                // 重置后更新图表
+                this.$refs.knowledgeGraphRef.$nextTick(() => {
+                    if (this.$refs.knowledgeGraphRef.activeView === 'graph' && this.$refs.knowledgeGraphRef.chart) {
+                        this.$refs.knowledgeGraphRef.updateChart();
+                    }
+                });
+            }
+            
+            // 根据tab切换调用新API,传递对应的knowledgeType和重置后的scoreRateTypes=null
+            this.getStudentKnowledgeDataTree(knowledgeType, null);
+            
+            if(!this.knowledgeId){
+                this.historicalChangeData = {
+                    personalList: [],
+                    classList: [],
+                    gradeList: [],
+                    examName: []
+                };
+                this.$nextTick(() => {
+                    if (this.$refs.historicalChangeChartRef) {
+                        this.$refs.historicalChangeChartRef.initChart();
+                    }
+                });
+                return
+            }
+            // 点击知识点后,更新历次变化
+            this.KnowledgeTrackData();
+            // 点击知识点后,更新推题列表
+            this.pushQuestionData();
+        },
+
+        // 导出精准提升试题
+        exportKnowledgePaps() {
+            if(this.knowledgData.length === 0){
+                this.$message.warning('暂无知识点推题');
+                return
+            }
+            // 触发Download组件弹窗
+            this.visible = true;
+        }
+    },
+}
+</script>
+
+<style scoped lang="scss">
+.personalProfile {
+
+    // 确保所有子组件之间有10px的间隔
+    >* {
+        margin-bottom: 10px;
+        font-family: PingFang SC, PingFang SC;
+        font-size: 14px;
+        font-weight: 400;
+        color: #333333;
+    }
+
+    >*:last-child {
+        margin-bottom: 0;
+    }
+
+    // 画像切换
+    .report_header {
+        padding: 10px 0;
+        text-align: center;
+        font-size: 14px;
+        background: #ffffff;
+        border-radius: 10px;
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        padding-left: 10px;
+
+        /* 选中状态 */
+        :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
+            font-weight: 600 !important;
+            color: #ffffff !important;
+        }
+
+        :deep(.el-radio-button__inner) {
+            height: 36px;
+            line-height: 34px;
+            padding: 0px;
+            width: 76px;
+            text-align: center;
+            color: #999999;
+        }
+    }
+}
+</style>

+ 260 - 0
src/views/analysisReport/personalProfile/knowledgePaps.vue

@@ -0,0 +1,260 @@
+<template>
+    <div class="knowledgePaps">
+        <div class="chart_title">
+            <div class="title_left">
+                <span class="secondary_title" style="font-weight: bold;">3、</span>
+                <span class="secondary_title" v-if="titleParts.knowledgeName">{{ titleParts.knowledgeName }} / </span>
+                <span class="main_title">薄弱知识点精准提升</span>
+            </div>
+            <button class="export_btn" @click="$emit('export-knowledge-paps')">
+                <i class="iconfont icon_export"></i>
+                导出精准提升试题
+            </button>
+        </div>
+
+        <div class="data_container" v-if="knowledgData.length > 0">
+            <div class="question_item" v-for="(item, index) in knowledgData" :key="index">
+                <!-- 推题头部 -->
+                <div class="question_header">
+                    <div class="question_meta">
+                        <span class="question_num">{{ index + 1 }}</span>
+                        <span class="question_type">题型类型:
+                            <i class="typeColor" style="color:#333333;font-weight: bold;">{{ item.questionType }}</i>
+                        </span>
+                        <span class="question_difficulty">难度:
+                            <i class="typeColor" style="color:#fac858;font-weight: bold;">{{ item.questionLevel }}</i>
+                        </span>
+                    </div>
+                    <div class="question_source">来源:
+                        <i class="typeColor" style="color:#999999;">{{ item.fromPaper }}</i>
+                    </div>
+                </div>
+                <!-- 推题内容 -->
+                <div class="question_content">
+                    <p v-html ='item.title || "" '></p>
+                </div>
+                <!-- 推题底部 -->
+                <div class="question_tags">
+                    <span class="tags_label">知识点:</span>
+                    <span class="tag" v-for="(knowledge, index) in (item.knowledge || '').split('、')" :key="index" v-if="knowledge">
+                        {{ knowledge }}
+                    </span>
+                </div>
+            </div>
+        </div>
+
+        <div 
+           class="module_chart no_content_data no_data"  
+           element-loading-background="#ffffff" v-else>
+          <span>暂无数据</span> 
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    name: "knowledgePaps",
+    props: {
+        // 知识点名称
+        knowledgeName: {
+            type: String,
+            default: ''
+        },
+        // 薄弱知识点精准提升数据
+        knowledgData: {
+            type: Array,
+            default: () => []
+        }
+    },
+    computed: {
+        // 标题部分
+        titleParts() {
+            return {
+                knowledgeName: this.knowledgeName
+            }
+        }
+    },
+    data() {
+        return {
+        }
+    },
+    mounted() {
+    },
+    watch: {
+        // knowledgData: {
+        //     handler(newVal, oldVal) {
+        //         console.log(newVal);
+        //     },
+        //     deep: true
+        // }
+    },
+    methods: {
+        
+    },
+}
+</script>
+<style scoped lang="scss">
+.knowledgePaps {
+    background: #FFFFFF;
+    border-radius: 10px;
+    padding: 20px;
+    margin-bottom: 10px;
+    position: relative;
+
+    // 标题部分
+    .chart_title {
+        font-size: 16px;
+        margin-bottom: 20px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        flex-wrap: wrap;
+        gap: 10px;
+
+        .title_left {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+        }
+
+        .main_title {
+            font-weight: 600;
+            color: #333;
+        }
+
+        .secondary_title {
+            font-weight: normal;
+            color: #999;
+        }
+
+        .export_btn {
+            display: flex;
+            align-items: center;
+            gap: 5px;
+            width: 152px;
+            height: 36px;
+            line-height: 36px;
+            background: #2E64FA;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            font-size: 14px;
+            cursor: pointer;
+            padding:0;
+            transition: all 0.3s ease;
+
+            &:hover {
+                background: #5883fb;
+            }
+
+            &:active {
+                background: #2E64FA;
+            }
+
+            .iconfont {
+                width: 14px;
+                margin-left: 10px;
+            }
+        }
+    }
+
+    // 内容部分
+    .question_item {
+        background: #FFFFFF;
+        border-radius: 10px 10px 10px 10px;
+        border: 1px solid #DCDFE6;
+        padding: 20px;
+        margin-bottom: 10px;
+        transition: all 0.3s ease;
+        &:hover {
+            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+        }
+        // 试题标题
+        .question_header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            padding-bottom: 20px;
+            flex-wrap: wrap;
+            gap: 10px;
+            border-bottom: 1px solid #EBEEF5;
+            .question_meta {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                flex-wrap: wrap;
+
+                .question_num {
+                    display: inline-block;
+                    width: 24px;
+                    height: 24px;
+                    line-height: 24px;
+                    background: #2E64FA;
+                    color: white;
+                    border-radius: 4px;
+                    text-align: center;
+                    font-size: 14px;
+                }
+
+                .question_type,
+                .question_difficulty {
+                    font-size: 14px;
+                    color: #999999;
+                }
+            }
+
+            .question_source {
+                font-size: 14px;
+                color: #999;
+            }
+        }
+        // 试题内容
+        .question_content {
+            padding: 20px 0;
+            border-bottom: 1px solid #EBEEF5;
+            p {
+                font-size: 16px;
+                color: #333333;
+                margin-bottom: 10px;
+                line-height: 2; 
+            }
+        }
+        // 试题底部
+        .question_tags {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            flex-wrap: wrap;
+            margin-top: 20px;
+            .tags_label {
+                font-size: 14px;
+                color: #666;
+            }
+            .tag {
+                display: inline-block;
+                padding: 4px 10px;
+                background: rgba(59,162,114,0.1);
+                border-radius: 4px 4px 4px 4px;
+                border: 1px solid #3BA272;
+                color: #3BA272;
+            }
+        }
+    }
+    .question_item:last-child {
+        margin-bottom: 0;
+    }
+
+    // 暂无数据
+    .no_data{
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 100%;
+        height: 220px;
+        font-size: 14px;
+        span{
+            margin-top: 11%;
+        }
+    }
+}
+
+</style>

+ 59 - 0
src/views/analysisReport/personalProfile/tools.js

@@ -0,0 +1,59 @@
+import { Loading } from 'element-ui';
+
+/**
+ * 从 HTTP 头的 Content-Disposition 字段中提取文件名
+ * @param {string} contentDisposition - HTTP 头的 Content-Disposition 字段值
+ * @returns {string|null} 提取的文件名,若未找到则返回 null
+*/
+export function ExtractFilename(contentDisposition) {
+    if (!contentDisposition) return null;
+
+    // 匹配 filename= 后面的内容(直到分号或字符串结束)
+    const match = contentDisposition.match(/filename=([^;]+)/i);
+    if (!match) return null;
+
+    let filename = match[1].trim();
+
+    // 去除可能的引号
+    if (filename.startsWith('"') && filename.endsWith('"')) {
+        filename = filename.slice(1, -1);
+    }
+
+    try {
+        // URL 解码
+        return decodeURIComponent(filename);
+    } catch (e) {
+        console.warn('URL解码失败,返回原始文件名:', e);
+        return filename;
+    }
+}
+
+
+/**
+ * 触发浏览器下载文件
+ * @param {Blob} blob - 要下载的文件的 Blob 对象
+ * @param {string} fileName - 下载时使用的文件名
+*/
+export function DownloadFile(blob, fileName) {
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = fileName;
+    link.click();
+    window.URL.revokeObjectURL(url);
+    link.remove();
+}
+
+/**
+ * 创建一个加载中实例
+ * @param {Object} options - 加载中配置选项
+ * @returns {Object} 加载中实例
+*/
+export function CreateLoadingInstance(options) {
+    options = Object.assign({
+        lock: true,
+        text: '正在处理...',
+    }, options || {});
+
+    return Loading.service(options);
+}

+ 1914 - 0
src/views/analysisReport/personalProfile/zeroScoreKnowledge.vue

@@ -0,0 +1,1914 @@
+<template>
+  <div class="knowledge_graph">
+
+
+    <div class="graph_header" v-if="activeView === 'list'">
+      <!-- 自定义图例 -->
+      <div class="legend">
+        <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
+          <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
+              }}</span>
+        </div>
+        <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
+          <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
+              }}</span>
+        </div>
+        <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }" @click="toggleLegend('excellent')">
+          <span class="legend_dot excellent"
+            :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
+            }}</span>
+        </div>
+      </div>
+      <!-- 视图切换按钮 - 移到knowledge_graph容器下 -->
+      <div class="view_switcher_container">
+        <el-button type="text" @click="switchView('graph')" class="switch_btn list_btn">
+          <i class="icon_switch_graph">
+            <img src="../../../assets/studentAnalysis/circleGrey.svg" alt="按图形查看">
+          </i>
+          按图形查看
+        </el-button>
+        <el-button type="graph" class="switch_btn graph_btn" @click="switchView('list')">
+          <i class="icon_switch_list">
+            <img src="../../../assets/studentAnalysis/iconBlue.svg" alt="按列表查看">
+          </i>
+          按列表查看
+        </el-button>
+      </div>
+    </div>
+
+
+    <!-- 图形查看容器 -->
+    <div v-if="activeView === 'graph'" class="graph_content">
+      <!-- 左侧图表 -->
+      <div class="chart_container">
+        <!-- 顶部容器:图例和视图切换按钮 -->
+        <div class="top_container">
+          <!-- 自定义图例 -->
+          <div class="legend">
+            <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
+              <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
+                  }}</span>
+            </div>
+            <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
+              <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
+                  }}</span>
+            </div>
+            <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }"
+              @click="toggleLegend('excellent')">
+              <span class="legend_dot excellent"
+                :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
+                }}</span>
+            </div>
+          </div>
+          <!-- 视图切换按钮  -->
+          <div class="view_switcher_container" v-if="activeView === 'graph'" :class="activeView">
+            <el-button type="graph" @click="switchView('graph')" class="switch_btn graph_btn">
+              <i class="icon_switch_graph">
+                <img src="../../../assets/studentAnalysis/circleBlue.svg" alt="按图形查看">
+              </i>
+              按图形查看
+            </el-button>
+            <el-button type="text" class="switch_btn list_btn" @click="switchView('list')">
+              <i class="icon_switch_list"><img src="../../../assets/studentAnalysis/iconGrey.svg" alt="按列表查看"></i>
+              按列表查看
+            </el-button>
+          </div>
+        </div>
+        <div ref="chart" class="chart" style="width: 100%;"></div>
+      </div>
+
+      <!-- 右侧容器,包含tab和知识点列表 -->
+      <div class="knowledge_right_container">
+        <!-- Tab切换 -->
+        <div class="knowledge_tab">
+          <div class="tab_item" :class="{ active: activeTab === 'all' }" @click="activeTab = 'all'">
+            全部知识点
+          </div>
+          <div class="tab_item" :class="{ active: activeTab === 'highFreq' }" @click="activeTab = 'highFreq'">
+            高频错题知识点
+          </div>
+          <div class="tab_item" :class="{ active: activeTab === 'zero' }" @click="activeTab = 'zero'">
+            零分知识点
+          </div>
+        </div>
+
+        <!-- 右侧知识点列表 -->
+        <div class="knowledge_list">
+          <!-- 暂无数据提示 -->
+          <div class="module_chart no_content_data no_data" style="height: 240px; " element-loading-background="#ffffff"
+            v-if="!knowledgeItems || knowledgeItems.length === 0">
+            <span>暂无数据</span>
+          </div>
+          <!-- 正常列表数据 -->
+
+          <div class="list_item" v-for="(item, index) in knowledgeItems" :key="index"
+            @click="handleItemClick(item, index)" :class="{ active: selectedIndex === index }" v-else>
+            <div class="item_header">
+              <span class="item_dot" :style="{ backgroundColor: getDotColor(item, 'personalScoreRate') }"></span>
+              <el-tooltip :content="item.knowledgeName" placement="top" effect="light"
+                :disabled="!item.knowledgeName || item.knowledgeName.length < 15">
+                <span class="item_title">{{ item.knowledgeName }}</span>
+              </el-tooltip>
+              <!-- <span class="item_tag" v-if="item.scoreRateDiff"
+                :style="{ backgroundColor: parseFloat(item.scoreRateDiff) > 0 ? '#3BA272' : '#F56C6C' }">
+                <i>{{ item.scoreRateDiff }}% </i>
+                <i v-if="parseFloat(item.scoreRateDiff) !== 0" style="margin-top: 0px">{{ parseFloat(item.scoreRateDiff) > 0 ? '↑' : '↓' }}</i>
+              </span> -->
+              <span class="item_tag" v-if="item.isPush == 1" style="background-color: #2E64FA;">
+                有推题
+              </span>
+            </div>
+
+            <div class="item_info">
+              <!-- 班级/学生得分率 -->
+              <span class="item_score">
+                <span class="score_label" v-if="item.personalScoreRate">个人得分率:</span>
+                <span :style="{ color: getDotColor(item, 'personalScoreRate') }" v-if="item.personalScoreRate">{{
+                  item.personalScoreRate }}%</span>
+                <!-- <span class="score_separator" v-if="item.classScoreRate">|</span>
+                <span class="score_label">班级得分率:</span>
+                <span v-if="item.classScoreRate" class="score_second">{{ item.classScoreRate }}%</span>
+                <span v-if="!item.classScoreRate" :style="{ color: getDotColor(item, 'classScoreRate') }">{{
+                  item.classScoreRate }}%</span> -->
+              </span>
+
+              <!-- 班级/学生得分率 -->
+              <span class="item_exam_count">考试数: <span class="exam_count">{{ item.examNum || 0 }}</span></span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 列表查看容器 -->
+    <div v-if="activeView === 'list'" class="list_content">
+
+      <vxe-table ref="treeTable" :data="tableData" style="width: 100%;" maxHeight="480px" border show-header
+        :tree-config="{ childrenField: 'children', hasChildField: 'hasChildren', showIcon: true, iconOpen: 'el-icon-remove', iconClose: 'el-icon-circle-plus' }"
+        :scroll-y="{ enabled: true, gt: 480 }" fixed-header :header-cell-style="{ color: '#333' }"
+        @row-click="handleRowClick" @cell-click="handleCellClick" :row-class-name="rowClassName">
+
+        <vxe-table-column type="tree" tree-node prop="title" :title="subjectName">
+          <template #default="{ row }">
+            <div class="knowledge_item">
+              <span class="knowledge_title">{{ row.knowledgeName }}</span>
+            </div>
+          </template>
+        </vxe-table-column>
+
+        <!-- 个人得分率 -->
+        <vxe-table-column prop="personalScoreRate" title="个人得分率" width="180" align="center">
+          <template #default="{ row }">
+            <div class="rate_info" v-if="row.personalScoreRate !== null">
+              <span class="rate_dot" :class="getRateClass(row.personalScoreRate)"></span>
+              <span class="rate_value">{{ row.personalScoreRate }}%</span>
+            </div>
+          </template>
+        </vxe-table-column>
+        <!-- 个人知识点状态 -->
+        <vxe-table-column prop="personalScoreRate" title="个人知识点状态" width="120" align="center">
+          <template #default="{ row }">
+            <div class="status_info" v-if="row.personalScoreRate !== null">
+              <span class="status_value"
+                :style="{ color: getRateName(row.personalScoreRate) === '优秀' ? '#3BA272' : getRateName(row.personalScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
+                {{ getRateName(row.personalScoreRate) }}
+              </span>
+            </div>
+          </template>
+        </vxe-table-column>
+
+        <!-- 班级得分率 -->
+        <!-- <vxe-table-column prop="classScoreRate" title="班级得分率" width="180" align="center">
+          <template #default="{ row }">
+            <div class="rate_info" v-if="row.classScoreRate !== null">
+              <span class="rate_dot" :class="getRateClass(row.classScoreRate)"></span>
+              <span class="rate_value">{{ row.classScoreRate }}%</span>
+            </div>
+          </template>
+        </vxe-table-column> -->
+        <!-- 班级知识点状态 -->
+        <!-- <vxe-table-column prop="classScoreRate" title="班级知识点状态" width="120" align="center">
+          <template #default="{ row }">
+            <div class="status_info" v-if="row.classScoreRate !== null">
+              <span class="status_value"
+                :style="{ color: getRateName(row.classScoreRate) === '优秀' ? '#3BA272' : getRateName(row.classScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
+                {{ getRateName(row.classScoreRate) }}
+              </span>
+            </div>
+          </template>
+        </vxe-table-column> -->
+
+        <!-- 只有当班级名称不是年级且不为空时,才显示得分率差列 -->
+        <!-- <vxe-table-column prop="diff" title="得分率差" width="150" align="center">
+          <template #default="{ row }">
+            <div class="rate_info" v-if="row.scoreRateDiff !== null">
+              <span class="rate_dot" :class="getRateClass(row.scoreRateDiff)"></span>
+              <span class="diff_value"
+                :class="{ 'diff_negative': row.scoreRateDiff !== null && row.scoreRateDiff < 0 }">
+                {{ row.scoreRateDiff !== null && row.scoreRateDiff > 0 ? '+' : '' }}{{ row.scoreRateDiff }}%
+              </span>
+            </div>
+          </template>
+        </vxe-table-column> -->
+      </vxe-table>
+    </div>
+  </div>
+</template>
+
+<script>
+// 引入echarts
+import * as echarts from 'echarts';
+
+export default {
+  name: 'KnowledgeGraph',
+  props: {
+    activeView: { // 当前视图,graph或list 年级画像还是学生画像
+      type: String,
+      default: 'graph',
+      validator: (value) => {
+        return ['graph', 'list'].includes(value);
+      }
+    },
+    subjectName: {  // 科目名称
+      type: String,
+      default: '知识点'
+    },
+    subjectScoreRate: { // 科目得分率
+      type: Number,
+      default: 0
+    },
+    tableData: { // 知识点表格数据
+      type: Array,
+      default: () => []
+    },
+    fatalVulnerability: { // 零分知识点数据
+      type: Array,
+      default: () => []
+    },
+    highVulnerability: { // 高频错题知识点数据
+      type: Array,
+      default: () => []
+    },
+    allKnowledgeList: { // 全部知识点数据
+      type: Array,
+      default: () => []
+    },
+    classLevel: { // 判断是否是组合还是班级,还是年级
+      type: [String, Number],
+      default: ''
+    },
+    classGroupName: { // 班级组合名称
+      type: String,
+      default: ''
+    },
+    currentKnowledgeId: { // 当前选中的知识点ID
+      type: [String, Number],
+      default: ''
+    },
+  },
+  computed: {
+    // 根据当前tab返回对应的数据
+    knowledgeItems() {
+      if (this.activeTab === 'zero') {
+        return this.fatalVulnerability || [];
+      } else if (this.activeTab === 'highFreq') {
+        return this.highVulnerability || [];
+      } else if (this.activeTab === 'all') {
+        return this.allKnowledgeList || [];
+      }
+      return [];
+    }
+  },
+  data() {
+    return {
+      chart: null, // echarts实例
+      // 组件挂载状态标记
+      _isMounted: false,
+      // 首次渲染标志,用于控制初始化时的透明度逻辑
+      _isFirstRender: true,
+      // Tab切换状态
+      activeTab: 'all',
+      // 图例选中状态
+      selectedLegend: {
+        weak: true,
+        good: true,
+        excellent: true
+      },
+      // 选中的知识点索引
+      selectedIndex: 0,
+      // 当前选中的行数据
+      selectedRow: null,
+      // 当前选中的知识点ID,用于控制节点透明度
+      selectedKnowledgeId: '',
+      // 树形表格展开行keys
+      expandRowKeys: []
+    };
+  },
+  mounted() {
+    // 设置组件挂载状态
+    this._isMounted = true;
+
+    // 无论在什么模式下,只要默认视图是图形视图就初始化echarts图表
+    if (this.activeView === 'graph') {
+      this.$nextTick(() => {
+        this.initChart();
+      });
+    }
+  },
+  beforeDestroy() {
+    // 设置组件未挂载状态
+    this._isMounted = false;
+    // 销毁echarts实例
+    if (this.chart) {
+      this.chart.dispose();
+    }
+  },
+  watch: {
+    // 监听classLevel变化,当对比选择器数据变化时更新对比选项
+    classLevel: {
+      handler(newVal, oldVal) {
+
+      },
+      immediate: true
+    },
+    // 监听classGroupName变化,当班级组合名称变化时更新对比选项
+    classGroupName: {
+      handler(newVal, oldVal) {
+        if (newVal) {
+        }
+      },
+      immediate: true
+    },
+    // 监听activeView变化,当切换到图形视图时重新初始化图表,切换到列表视图时销毁图表
+    activeView(newView, oldView) {
+      if (newView === 'graph') {
+        this.$nextTick(() => {
+          this.initChart();
+        });
+      } else if (newView === 'list') {
+        // 切换到列表视图时销毁图表实例
+        if (this.chart) {
+          this.chart.dispose();
+          this.chart = null;
+        }
+        // 切换到列表视图时,展开所有树形节点
+        this.$nextTick(() => {
+          this.expandAllNodes();
+        });
+      }
+    },
+    // 监听tab切换,更新选中索引并通知父组件
+    activeTab(newTab, oldTab) {
+      this.selectedIndex = 0;
+
+      // 根据当前tab获取对应的数据列表
+      const currentList = this.knowledgeItems;
+      // 如果有数据,更新selectedKnowledgeId为第一条数据的ID
+      if (currentList && currentList.length > 0) {
+        this.selectedKnowledgeId = currentList[0].knowledgeId;
+        // 如果当前是图形视图且图表已初始化,更新图表选中状态
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      } else {
+        // 如果没有数据,清空selectedKnowledgeId
+        this.selectedKnowledgeId = '';
+      }
+
+      // 只有当组件已挂载且tab值实际发生变化,并且不是初始化时,才向父组件发送事件
+      // 这是为了避免在视图切换时重复调用接口
+      if (this._isMounted && newTab !== oldTab) {
+        // 向父组件发送tab切换事件
+        this.$emit('activeTabChange', newTab);
+      }
+    },
+    // 监听数据变化,重新设置默认选中项
+    fatalVulnerability: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    highVulnerability: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    allKnowledgeList: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    // 监听tableData变化,更新展开行
+    tableData: {
+      deep: true,
+      handler() {
+        // 当tableData变化时,重新展开所有节点
+        if (this.activeView === 'list') {
+          this.$nextTick(() => {
+            this.expandAllNodes();
+          });
+        }
+        if (this.chart) {
+          this.$nextTick(() => {
+            this.updateChart();
+          });
+        }
+      }
+    },
+    // 监听当前选中的知识点ID变化,更新图表高亮状态
+    currentKnowledgeId: {
+      handler(newVal) {
+        // 更新selectedKnowledgeId,确保图表使用正确的高亮状态
+        this.selectedKnowledgeId = newVal;
+        // 仅当当前视图是图形视图且图表已初始化时才更新
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      }
+    }
+  },
+  methods: {
+    // 设置默认选中项
+    setDefaultSelection() {
+      // 保存当前tab值,用于比较是否需要修改
+      const oldActiveTab = this.activeTab;
+      let selectedItem = null;
+
+      // 优先级:全部知识点> 高频错题知识点 > 零分知识点 
+      if (this.allKnowledgeList && this.allKnowledgeList.length > 0) {
+        // 如果前两者都没有数据但全部知识点有数据,切换到全部知识点并选中第一条
+        if (this.activeTab !== 'all') {
+          this.activeTab = 'all';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.allKnowledgeList[0];
+      } else if (this.highVulnerability && this.highVulnerability.length > 0) {
+        // 如果全部知识点没有数据但高频错题知识点有数据,切换到高频错题知识点并选中第一条
+        if (this.activeTab !== 'highFreq') {
+          this.activeTab = 'highFreq';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.highVulnerability[0];
+      } else if (this.fatalVulnerability && this.fatalVulnerability.length > 0) {
+        // 如果零分知识点有数据,切换到零分知识点并选中第一条
+        if (this.activeTab !== 'zero') {
+          this.activeTab = 'zero';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.fatalVulnerability[0];
+      }
+
+      // 如果有选中项,更新selectedKnowledgeId,实现初始化默认选择
+      if (selectedItem) {
+        this.selectedKnowledgeId = selectedItem.knowledgeId;
+        // 如果当前是图形视图且图表已初始化,更新图表选中状态
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      }
+
+      // 只有当tab值实际发生变化时,才触发选中事件
+      // 初始化时不触发,避免重复调用接口
+      if (this._isMounted && oldActiveTab !== this.activeTab) {
+        // 组件已挂载且tab值发生变化时才发送事件
+      }
+    },
+
+    // 递归收集所有节点的key,用于展开所有树形节点
+    collectAllRowKeys() {
+      const keys = [];
+
+      // 递归函数,收集所有有children的节点key
+      const collectKeys = (data) => {
+        if (!data || !Array.isArray(data)) return;
+
+        data.forEach(item => {
+          // 如果节点有children,将其key添加到keys数组中
+          if (item.children && item.children.length > 0) {
+            // 使用knowledgeId作为key
+            keys.push(item.knowledgeId);
+            // 递归处理children
+            collectKeys(item.children);
+          }
+        });
+      };
+
+      // 调用递归函数
+      collectKeys(this.tableData);
+
+      // 更新expandRowKeys数组
+      this.expandRowKeys = keys;
+    },
+
+    // 使用vxe-table API展开所有树形节点
+    expandAllNodes() {
+      // 获取vxe-table实例
+      const treeTable = this.$refs.treeTable;
+      if (treeTable) {
+        // 递归收集所有需要展开的节点
+        const expandNodes = [];
+        const collectExpandNodes = (data) => {
+          if (!data || !Array.isArray(data)) return;
+          data.forEach(item => {
+            if (item.children && item.children.length > 0) {
+              expandNodes.push(item);
+              collectExpandNodes(item.children);
+            }
+          });
+        };
+        collectExpandNodes(this.tableData);
+
+        // 使用vxe-table 3.7.5兼容的API展开所有节点
+        if (treeTable.setExpandRows) {
+          // 如果支持setExpandRows方法
+          treeTable.setExpandRows(expandNodes);
+        } else if (treeTable.setTreeExpand) {
+          // 如果支持setTreeExpand方法
+          expandNodes.forEach(node => {
+            treeTable.setTreeExpand(node, true);
+          });
+        }
+      }
+    },
+
+    // 切换查看方式
+    switchView(view) {
+      // 通过emit事件通知父组件更新activeView
+      this.$emit('view-change', view);
+    },
+
+    // 年级、班级组合判断
+    classIsGroup() {
+      if (this.classLevel == 1) {
+        return true;
+      } else
+        return false;
+    },
+
+    // 切换图例选中状态
+    toggleLegend(type) {
+      this.selectedLegend[type] = !this.selectedLegend[type];
+
+      // 生成scoreRateTypes数组
+      let scoreRateTypes = [];
+      if (this.selectedLegend.weak) scoreRateTypes.push(1);
+      if (this.selectedLegend.good) scoreRateTypes.push(2);
+      if (this.selectedLegend.excellent) scoreRateTypes.push(3);
+
+      // 如果包含所有值,则传null,否则传数组
+      if (scoreRateTypes.length === 3) {
+        scoreRateTypes = null;
+      }
+
+      // 向父组件发送图例变化事件,包含scoreRateTypes和当前activeTab对应的knowledgeType
+      let knowledgeType = 0;
+      if (this.activeTab === 'highFreq') {
+        knowledgeType = 1;
+      } else if (this.activeTab === 'zero') {
+        knowledgeType = 2;
+      } else {
+        knowledgeType = 0;
+      }
+
+      this.$emit('legend-change', { scoreRateTypes, knowledgeType });
+
+    },
+
+    // 根据得分率获取对应的颜色
+    getNodeColor(scoreRate) {
+      const rate = parseFloat(scoreRate);
+      if (rate >= 85) {
+        return '#3BA272'; // 优秀 - 绿色
+      } else if (rate >= 60) {
+        return '#FAC858'; // 良好 - 黄色
+      } else {
+        return '#EE6666'; // 薄弱 - 红色
+      }
+    },
+
+    // 更新图表缩放和布局参数,防止窗口变化时图表变形
+    updateChartLayout() {
+      if (this.chart) {
+        // 重新计算最外层知识点数量
+        const countOutermostNodes = (data) => {
+          let count = 0;
+          const traverse = (nodes) => {
+            nodes.forEach(node => {
+              if (node.children && node.children.length > 0) {
+                traverse(node.children);
+              } else {
+                count++;
+              }
+            });
+          };
+          traverse(data);
+          return count;
+        };
+        
+        const outermostCount = countOutermostNodes(this.tableData);
+        
+        // 重新计算合适的缩放比例
+        const getOptimalZoom = () => {
+          const screenWidth = window.innerWidth;
+          let zoom = 1.05;
+          
+          if (screenWidth < 1100) {
+            zoom = 1.05;
+          } else if (screenWidth >= 1100 && screenWidth <= 1200) {
+            zoom = 1.05;
+          } else {
+            zoom = 1.05;
+          }
+          
+          if (outermostCount <= 5) {
+            zoom *= 1.0;
+          }
+          
+          return zoom;
+        };
+        
+        // 重新计算内外半径
+        const getOptimalRadius = () => {
+          if (outermostCount <= 5) {
+            return ['20%', '65%'];
+          }
+          return ['12%', '75%'];
+        };
+        
+        // 更新图表的所有动态参数
+        this.chart.setOption({
+          series: [{
+            // 更新缩放比例
+            zoom: getOptimalZoom(),
+            // 更新内外半径
+            radius: getOptimalRadius(),
+            // 更新层级间距
+            layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5]
+          }]
+        });
+      }
+    },
+
+    // 初始化echarts图表
+    initChart() {
+      try {
+        // 检查chart容器是否存在
+        if (!this.$refs.chart) {
+          console.error('图表容器不存在');
+          return;
+        }
+        this.createChart();
+      }
+      catch (error) {
+        console.error('图表初始化失败:', error);
+      }
+    },
+
+    // 创建图表
+    createChart() {
+      try {
+        // 检查chart容器是否存在
+        if (!this.$refs.chart) {
+          console.error('图表容器不存在');
+          return;
+        }
+        // 销毁现有的图表实例
+        if (this.chart) {
+          this.chart.dispose();
+        }
+        // 创建echarts实例
+        this.chart = echarts.init(this.$refs.chart);
+
+        // 保存当前组件实例的引用
+        const vm = this;
+
+        // 根据得分率获取对应的级别
+        const getNodeLevel = (scoreRate) => {
+          const rate = parseFloat(scoreRate);
+          // 处理scoreRate为null、undefined或非数字的情况
+          if (isNaN(rate)) {
+            return 'weak'; // 默认为薄弱
+          }
+          if (rate >= 85) {
+            return 'excellent'; // 优秀
+          } else if (rate >= 60) {
+            return 'good'; // 良好
+          } else {
+            return 'weak'; // 薄弱
+          }
+        };
+
+        // 基于表格数据生成树形图数据
+        // 添加一个可选参数overrideKnowledgeId,用于临时覆盖当前的currentKnowledgeId
+        const generateTreeData = (data, level = 0, overrideKnowledgeId = null) => {
+          // 使用传入的overrideKnowledgeId或默认使用vm.selectedKnowledgeId
+          const currentKnowledgeId = overrideKnowledgeId !== null ? overrideKnowledgeId : vm.selectedKnowledgeId;
+
+          return data.map(item => {
+            // 检查当前节点是否匹配选中的知识点ID
+            const isMatched = String(item.knowledgeId) === String(currentKnowledgeId);
+
+            // 获取节点级别,优先使用对应级别的得分率,若缺失则使用另一级别作为备选
+            const scoreRate = item.personalScoreRate;
+            const hasData = scoreRate !== undefined && scoreRate !== null && scoreRate !== '';
+            
+            // 设置节点大小:有数据的默认10px,选中后15px;无数据的5px
+            let symbolSize = 5;
+            if (hasData) {
+              symbolSize = isMatched ? 15 : 10;
+            }
+
+            // 计算节点样式
+            const itemColor = hasData ? vm.getNodeColor(scoreRate) : '#BFC1C7';
+            
+            // 获取节点级别用于图例过滤
+            const nodeLevel = getNodeLevel(scoreRate);
+            // 应用图例过滤
+            const shouldShow = vm.selectedLegend[nodeLevel];
+            
+            // 设置节点透明度:显示为1,隐藏为0
+            const opacity = shouldShow ? 1 : 0;
+            
+            const node = {
+              name: item.knowledgeName,
+              symbolSize: symbolSize,
+              // 使用已计算的scoreRate作为value,确保数据一致性
+              value: scoreRate,
+              itemStyle: {
+                color: itemColor,
+                opacity: opacity
+              },
+              // 当节点被隐藏时,隐藏连接到该节点的线
+              lineStyle: {
+                opacity: shouldShow ? 1 : 0
+              },
+              // 保存知识ID用于匹配
+              knowledgeId: item.knowledgeId
+            };
+
+            if (item.children && item.children.length > 0) {
+              node.children = generateTreeData(item.children, level + 1, overrideKnowledgeId);
+            }
+
+            return node;
+          });
+        };
+
+        // 计算最外层知识点数量(叶子节点数量),用于动态调整布局
+        const countOutermostNodes = (data) => {
+          let count = 0;
+          const traverse = (nodes) => {
+            nodes.forEach(node => {
+              if (node.children && node.children.length > 0) {
+                traverse(node.children);
+              } else {
+                count++;
+              }
+            });
+          };
+          traverse(data);
+          return count;
+        };
+        
+        // 获取最外层知识点数量
+        const outermostCount = countOutermostNodes(this.tableData);
+        
+        // 计算合适的缩放比例,根据屏幕宽度和节点数量动态调整
+        const getOptimalZoom = () => {
+          const screenWidth = window.innerWidth;
+          let zoom = 1.05;
+          
+          // 根据屏幕宽度调整缩放
+          if (screenWidth < 1100) {
+            zoom = 1.05;
+          } else if (screenWidth >= 1100 && screenWidth <= 1200) {
+            zoom = 1.05;
+          } else {
+            zoom = 1.05;
+          }
+          
+          // 根据节点数量调整缩放,节点少的时候适当放大
+          if (outermostCount <= 5) {
+            zoom *= 1.0;
+          }
+          
+          return zoom;
+        };
+        
+        // 根据节点数量动态调整内外半径,防止节点遮盖
+        const getOptimalRadius = () => {
+          // 节点数量少的时候,增大内半径,缩小外半径,使节点分布更合理
+          if (outermostCount <= 5) {
+            return ['20%', '65%'];
+          }
+          // 节点数量多的时候,使用默认半径
+          return ['12%', '75%'];
+        };
+        
+        // 配置项
+        const option = {
+          tooltip: {
+            formatter: (params) => {
+              // 检查是否为根节点
+              if (params.treePathInfo && params.treePathInfo.length === 1) {
+                // 根节点也属于非最外层节点,只显示名称
+                return params.name;
+              }
+
+              // 检查节点是否为最外层节点(叶子节点)
+              // 使用isOutermost属性或检查data.isOutermost
+              const isOutermost = params.data && params.data.isOutermost;
+
+              if (isOutermost) {
+                // 最外层节点显示完整信息(名称+得分率)
+                return `${params.name}<br/>得分率: ${params.value !== null && params.value !== undefined ? params.value : '0'}%`;
+              } else {
+                // 非最外层节点只显示名称
+                return params.name;
+              }
+            }
+          },
+          animationDurationUpdate: 1500,
+          animationEasingUpdate: 'quinticInOut',
+          series: [
+            {
+              type: 'tree',
+              layout: 'radial',
+              symbol: 'circle',
+              initialTreeDepth: 999, // 设置一个足够大的值,确保所有节点都展开
+              expandAndCollapse: false,
+              // 调整树状图布局参数,确保节点均匀分布
+              orient: 'radial',
+              roam: true,
+              
+              // 使用边距控制图表位置,实现水平垂直居中
+              left: '8%',   // 左边距(百分比/像素)
+              right: '8%',  // 右边距
+              top: '8%',    // 上边距
+              bottom: '8%', // 下边距
+
+              // 根据节点数量动态调整内外半径,防止节点遮盖
+              radius: getOptimalRadius(),
+              
+              // 动态计算合适的缩放比例,根据屏幕宽度和节点数量调整
+              zoom: getOptimalZoom(),
+              scaleLimit: {
+                min: 0.2,
+                max: 5
+              },
+
+              // 调整节点大小比例和间距,确保节点不遮盖
+              nodeScaleRatio: 1,
+              // 根据节点数量调整层级间距,节点少的时候增大间距
+              layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5],
+              roam: true,
+
+              // 使用表格数据生成树形结构
+              data: [
+                {
+                  name: this.subjectName,
+                  symbolSize: 5, // 根节点大小
+                  value: this.subjectScoreRate, // 添加value属性,用于显示得分率
+                  itemStyle: {
+                    // 根节点为非最外层节点,使用#EBEEF5颜色
+                    color: '#BFC1C7'
+                  },
+                  // 标记为非最外层节点
+                  isOutermost: false,
+                  children: generateTreeData(this.tableData)
+                }
+              ],
+              label: {
+                show: false
+              },
+              lineStyle: {
+                color: '#ECEEF3',
+                width: 1,
+                type: 'solid'
+              },
+              emphasis: {
+                focus: 'adjacency',
+                lineStyle: {
+                  width: 1
+                }
+              }
+            }
+          ]
+        };
+
+        // 设置配置项
+        this.chart.setOption(option);
+
+        // 初始化后调用resize确保图表正确显示
+        this.chart.resize();
+
+        // 添加图表点击事件监听
+        this.chart.on('click', (params) => {
+          // 明确识别点击类型
+          const isOuterNodeClick = params.data && params.data.isOutermost;
+          const isEmptyAreaClick = !params.data || !params.componentType || params.componentType === '';
+          const isNonOuterNodeClick = params.data && !params.data.isOutermost;
+
+          if (isOuterNodeClick) {
+            // 处理最外层节点点击
+            // 确定当前显示的列表数据
+            let targetList = [];
+            if (this.activeTab === 'zero') {
+              targetList = this.fatalVulnerability;
+            } else if (this.activeTab === 'highFreq') {
+              targetList = this.highVulnerability;
+            } else {
+              targetList = this.allKnowledgeList;
+            }
+
+            // 1. 首先尝试直接从当前列表中根据名称查找
+            let knowledgeItem = targetList.find(item => item.knowledgeName === params.name);
+            let targetIndex = -1;
+
+            if (knowledgeItem) {
+              targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
+            }
+
+            // 2. 如果在当前列表中找不到,尝试从其他列表中查找
+            if (!knowledgeItem) {
+              const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
+              for (const list of allLists) {
+                knowledgeItem = list.find(item => item.knowledgeName === params.name);
+                if (knowledgeItem) {
+                  // 找到后切换到对应tab
+                  if (list === this.highVulnerability) {
+                    this.activeTab = 'highFreq';
+                    targetList = this.highVulnerability;
+                  } else if (list === this.fatalVulnerability) {
+                    this.activeTab = 'zero';
+                    targetList = this.fatalVulnerability;
+                  } else {
+                    this.activeTab = 'all';
+                    targetList = this.allKnowledgeList;
+                  }
+                  targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
+                  break;
+                }
+              }
+            }
+
+            // 3. 如果还是找不到,尝试从tableData中查找
+            if (!knowledgeItem) {
+              const findKnowledgeItem = (data) => {
+                for (let item of data) {
+                  if (item.knowledgeName === params.name) {
+                    return item;
+                  }
+                  if (item.children && item.children.length > 0) {
+                    const found = findKnowledgeItem(item.children);
+                    if (found) return found;
+                  }
+                }
+                return null;
+              };
+
+              knowledgeItem = findKnowledgeItem(this.tableData);
+
+              if (knowledgeItem) {
+                // 在所有列表中查找匹配的knowledgeId
+                const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
+                for (const list of allLists) {
+                  targetIndex = list.findIndex(item => {
+                    // 考虑类型转换,确保比较准确
+                    return String(item.knowledgeId) === String(knowledgeItem.knowledgeId);
+                  });
+                  if (targetIndex !== -1) {
+                    // 切换到对应tab
+                    if (list === this.highVulnerability) {
+                      this.activeTab = 'highFreq';
+                      targetList = this.highVulnerability;
+                    } else if (list === this.fatalVulnerability) {
+                      this.activeTab = 'zero';
+                      targetList = this.fatalVulnerability;
+                    } else {
+                      this.activeTab = 'all';
+                      targetList = this.allKnowledgeList;
+                    }
+                    break;
+                  }
+                }
+              }
+            }
+
+            if (knowledgeItem && targetIndex !== -1) {
+              // 向父组件发送事件
+              this.$emit('knowledge-item-click', {
+                item: knowledgeItem,
+                index: targetIndex
+              });
+
+              // 更新选中索引和选中知识点ID
+              this.selectedIndex = targetIndex;
+              this.selectedKnowledgeId = knowledgeItem.knowledgeId;
+
+              // 将首次渲染标志设为false,后续点击操作将应用透明度降低逻辑
+              // this._isFirstRender = false;
+
+              // 重新生成图表数据,确保只有当前选中节点高亮
+              this.chart.setOption({
+                series: [{
+                  data: [
+                    {
+                      name: this.subjectName,
+                      symbolSize: 5,
+                      value: this.subjectScoreRate,
+                      itemStyle: {
+                        color: '#BFC1C7',
+                        opacity: 1
+                      },
+                      isOutermost: false,
+                      children: generateTreeData(this.tableData, 0, this.selectedKnowledgeId)
+                    }
+                  ]
+                }]
+              }, {
+                animation: true,
+                animationDuration: 300
+              });
+
+              // 滚动到选中项
+              this.$nextTick(() => {
+                const listContainer = this.$el.querySelector('.knowledge_list');
+                const listItems = listContainer.querySelectorAll('.list_item');
+                if (listItems[targetIndex]) {
+                  listContainer.scrollTop = listItems[targetIndex].offsetTop - 100;
+                }
+              });
+            } else {
+              // 打印详细调试信息
+              console.log('点击处理调试信息:', {
+                paramsName: params.name,
+                params: params,
+                knowledgeItem: knowledgeItem,
+                targetIndex: targetIndex,
+                activeTab: this.activeTab,
+                targetListLength: targetList.length,
+                targetListSample: targetList.slice(0, 3),
+                tableDataSample: this.tableData.slice(0, 1)
+              });
+            }
+          }
+          else if (isEmptyAreaClick || isNonOuterNodeClick) {
+            // 点击空白区域或非最外层节点,恢复所有节点透明度
+            // 清空selectedKnowledgeId,确保所有节点都高亮
+            this.selectedKnowledgeId = '';
+
+            // 调用generateTreeData时传入overrideKnowledgeId为空字符串,确保所有节点都不透明
+            const updatedOption = {
+              series: [{
+                data: [
+                  {
+                    name: this.subjectName,
+                    symbolSize: 5,
+                    value: this.subjectScoreRate,
+                    itemStyle: {
+                      color: '#BFC1C7',
+                      opacity: 1
+                    },
+                    isOutermost: false,
+                    // 传入overrideKnowledgeId为空字符串,确保生成的所有节点都不透明
+                    children: generateTreeData(this.tableData, 0, '')
+                  }
+                ]
+              }]
+            };
+            // 更新图表,使用动画实现平滑过渡
+            this.chart.setOption(updatedOption, {
+              animation: true,
+              animationDuration: 300
+            });
+
+            // 向父组件发送事件,清除选中的知识点ID
+            this.$emit('knowledge-item-click', {
+              item: null,
+              index: -1
+            });
+          }
+        });
+
+        // 监听窗口大小变化
+        window.addEventListener('resize', () => {
+          if (this.chart) {
+            this.chart.resize();
+            this.updateChartLayout();
+          }
+        });
+      }
+      catch (error) {
+        console.error('图表创建失败:', error);
+      }
+    },
+
+    // 更新图表数据
+    updateChart() {
+      try {
+        if (this.activeView === 'graph' && this.$refs.chart) {
+          // 重新创建图表,实现刷新效果
+          this.createChart();
+        }
+      }
+      catch (error) {
+        console.error('图表更新失败:', error);
+      }
+    },
+
+    // 根据得分率获取对应的样式类
+    getRateClass(rate) {
+      const rateValue = parseFloat(rate);
+      if (rateValue >= 85) {
+        return 'rate_excellent';
+      } else if (rateValue >= 60) {
+        return 'rate_good';
+      } else {
+        return 'rate_weak';
+      }
+    },
+
+    // 根据得分率获取对应的状态名称
+    getRateName(rate) {
+      const rateValue = parseFloat(rate);
+      if (rateValue >= 85) {
+        return '优秀';
+      } else if (rateValue >= 60) {
+        return '良好';
+      } else {
+        return '薄弱';
+      }
+    },
+
+    // 行样式类名方法 - vxe-table 3.7.5版本兼容
+    rowClassName({ row }) {
+      // 使用selectedRow引用比较,避免修改数据
+      if (this.selectedRow && this.selectedRow.knowledgeId === row.knowledgeId) {
+        return 'selected-row';
+      }
+      return row.highlight ? 'row_highlight' : '';
+    },
+
+    // 处理知识点列表项点击事件
+    handleItemClick(item, index) {
+      // 更新选中索引
+      this.selectedIndex = index;
+      // 更新选中知识点ID,实现反向选择功能
+      this.selectedKnowledgeId = item.knowledgeId;
+      // 向父组件发送事件,传递点击的知识点数据
+      this.$emit('knowledge-item-click', { item, index });
+
+      // 如果当前是图形视图且图表已初始化,更新图表选中状态
+      if (this.activeView === 'graph' && this.chart) {
+        this.updateChart();
+      }
+    },
+
+    // 处理树形表格行点击事件
+    handleRowClick(row) {
+      // 检查是否为最后一级节点(没有children或children为空)
+      const isLastLevel = !row.children || row.children.length === 0;
+
+      if (isLastLevel) {
+        // 设置当前行为选中行,不修改数据,只更新引用
+        this.selectedRow = row;
+
+        // 调用handleItemClick方法处理点击事件
+        this.handleItemClick(row, -1);
+      }
+    },
+
+    // 处理单元格点击事件
+    handleCellClick({ row }) {
+      // 调用行点击事件处理逻辑
+      this.handleRowClick(row);
+    },
+
+    // 根据知识点得分率获取对应的点颜色
+    getDotColor(item, scoreRateType) {
+      // 个人得分率personalScoreRate
+      const scoreRate = scoreRateType === 'personalScoreRate' ? item.personalScoreRate : item.classScoreRate;
+      const rate = parseFloat(scoreRate);
+      if (rate >= 85) {
+        return '#3BA272'; // 优秀 - 绿色
+      } else if (rate >= 60 && rate < 84) {
+        return '#FAC858'; // 良好 - 黄色
+      } else if (rate < 59) {
+        return '#EE6666'; // 薄弱 - 红色
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.knowledge_graph {
+  background: #FFFFFF;
+  border-radius: 10px;
+  padding: 20px;
+  margin-bottom: 10px;
+  position: relative;
+
+  /* 选中行样式 - 确保所有情况下都能正确应用 */
+  :deep(.vxe-body--row.selected-row),
+  :deep(.selected-row) {
+    background-color: rgba(46, 100, 250, 0.1) !important;
+    color: #2E64FA !important;
+
+    /* 确保所有子元素也继承文字颜色 */
+    * {
+      color: #2E64FA !important;
+    }
+
+    /* 特殊处理评分点样式 */
+    .rate_dot {
+      border-color: rgba(46, 100, 250, 0.1) !important;
+    }
+  }
+
+  .graph_header {
+    margin-bottom: 20px;
+    position: relative;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .legend {
+      display: flex;
+      gap: 20px;
+
+      .legend_item {
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        cursor: pointer;
+        opacity: 0.7;
+        transition: all 0.3s ease;
+        font-weight: 500;
+
+        &.selected {
+          opacity: 1;
+        }
+
+        &:hover {
+          opacity: 1;
+        }
+
+        .legend_dot {
+          width: 20px;
+          height: 10px;
+          border-radius: 2px;
+          transition: all 0.3s ease;
+          background: #999999; // 默认灰色
+
+          &.weak {
+            background: #EE6666;
+          }
+
+          &.good {
+            background: #FAC858;
+          }
+
+          &.excellent {
+            background: #3BA272;
+          }
+        }
+
+        .legend_text {
+          font-size: 12px;
+          color: #999999; // 默认灰色文字
+          transition: all 0.3s ease;
+        }
+
+        // 选中状态样式
+        &.selected {
+          .legend_dot.weak {
+            background: #EE6666;
+          }
+
+          .legend_dot.good {
+            background: #FAC858;
+          }
+
+          .legend_dot.excellent {
+            background: #3BA272;
+          }
+
+          .legend_text {
+            color: #333;
+          }
+        }
+      }
+
+    }
+  }
+
+  // 视图切换按钮样式
+  .view_switcher_container {
+    display: flex;
+    gap: 20px;
+    z-index: 10;
+    justify-content: flex-end;
+    font-size: 14px;
+
+    .switch_btn {
+      display: flex;
+      align-items: center;
+      padding: 0;
+      background-color: transparent;
+      border: none;
+      border-radius: 0;
+      font-size: 14px;
+      transition: color 0.3s ease;
+      cursor: pointer;
+      font-weight: 400;
+      color: #999999;
+
+      &:hover {
+        color: #2E64FA;
+      }
+    }
+
+    .graph_btn {
+      color: #2E64FA;
+    }
+
+    // 图标样式,确保与文字对齐
+    .icon_switch_graph,
+    .icon_switch_list {
+      img {
+        display: inline-block;
+        vertical-align: middle;
+        margin: 0;
+        padding: 0;
+        margin-top: -3px;
+      }
+    }
+
+    // 鼠标悬停时图片换色效果
+    .switch_btn:hover .icon_switch_graph img,
+    .switch_btn:hover .icon_switch_list img {
+      filter: brightness(0) saturate(100%) invert(34%) sepia(100%) saturate(5000%) hue-rotate(210deg) brightness(95%) contrast(100%);
+    }
+  }
+
+  // 对比选择器样式
+  .comparison_selector {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    .student_position {
+      font-size: 14px;
+      color: #999999;
+    }
+
+    // 对比选择器按钮样式
+    :deep(.el-button-group) {
+      .el-button {
+        padding: 7px 10px;
+        // border-radius: 4px;
+
+        &:not(.el-button--primary) {
+          color: #999999;
+          background-color: #FFFFFF;
+          border-color: #DCDFE6;
+
+          &:hover {
+            color: #2E64FA;
+            border-color: #C6E2FF;
+          }
+        }
+
+        &.el-button--primary {
+          background-color: #2E64FA;
+          color: #FFFFFF;
+          border-color: #2E64FA;
+
+          &:hover {
+            background-color: #409EFF;
+            border-color: #409EFF;
+          }
+        }
+      }
+    }
+  }
+
+  .graph_content {
+    display: flex;
+    gap: 20px;
+    position: relative;
+
+    .knowledge_tab {
+      display: flex;
+      border-bottom: 1px solid #ECEEF3;
+      margin-bottom: 15px;
+
+      .tab_item {
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        color: #666666;
+        position: relative;
+        transition: all 0.3s ease;
+        margin-right: 20px;
+
+        &.active {
+          color: #2E64FA;
+          font-weight: 500;
+
+          &::after {
+            content: '';
+            position: absolute;
+            bottom: -1px;
+            left: 0;
+            width: 100%;
+            height: 2px;
+            background-color: #2E64FA;
+          }
+        }
+
+        &:hover {
+          color: #2E64FA;
+        }
+
+        // 零分知识点:根据文字宽度自适应,不需要内间距
+        // &:first-child {
+        //   padding: 0;
+        //   white-space: nowrap;
+        // }
+
+        // // 高频错题知识点:占满剩余宽度
+        // &:last-child {
+        //   padding: 0;
+        //   white-space: nowrap;
+        // }
+      }
+    }
+
+    .knowledge_right_container {
+      width: 350px;
+      display: flex;
+      flex-direction: column;
+    }
+
+    .chart_container {
+      flex: 1;
+      min-width: 0;
+      border-radius: 10px 10px 10px 10px;
+      border: 1px solid #E9EBEF;
+      position: relative;
+      padding: 20px;
+      overflow: hidden;
+
+      .top_container {
+        display: flex;
+        justify-content: space-between;
+        align-items: flex-start;
+        flex-wrap: wrap;
+        gap: 10px;
+        margin-bottom: 10px;
+      }
+
+      .legend {
+        display: flex;
+        gap: 20px;
+        flex-wrap: wrap; // 小屏幕下自动换行
+        flex: 1;
+        min-width: 0;
+
+        .legend_item {
+          display: flex;
+          align-items: center;
+          gap: 5px;
+          cursor: pointer;
+          opacity: 0.7;
+          transition: all 0.3s ease;
+          font-weight: 500;
+
+          &.selected {
+            opacity: 1;
+          }
+
+          &:hover {
+            opacity: 1;
+          }
+
+          .legend_dot {
+            width: 20px;
+            height: 10px;
+            border-radius: 2px;
+            transition: all 0.3s ease;
+            background: #999999; // 默认灰色
+
+            &.weak {
+              background: #EE6666;
+            }
+
+            &.good {
+              background: #FAC858;
+            }
+
+            &.excellent {
+              background: #3BA272;
+            }
+          }
+
+          .legend_text {
+            font-size: 12px;
+            color: #999999; // 默认灰色文字
+            transition: all 0.3s ease;
+          }
+
+          // 选中状态样式
+          &.selected {
+            .legend_dot.weak {
+              background: #EE6666;
+            }
+
+            .legend_dot.good {
+              background: #FAC858;
+            }
+
+            .legend_dot.excellent {
+              background: #3BA272;
+            }
+
+            .legend_text {
+              color: #333;
+            }
+          }
+        }
+
+      }
+
+      .chart {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        min-height: 590px;
+      }
+
+      .view_switcher_container {
+        display: flex;
+        gap: 20px;
+        z-index: 10;
+        justify-content: flex-end;
+        font-size: 14px;
+        white-space: nowrap; // 防止按钮换行
+      }
+    }
+
+    .knowledge_list {
+      width: 100%;
+      height: 630px;
+      overflow-y: auto;
+      padding-right: 10px;
+      position: relative;
+
+      /* 滚动条样式 */
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+
+      &::-webkit-scrollbar-track {
+        background: #f1f1f1;
+        border-radius: 3px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background: #c1c1c1;
+        border-radius: 3px;
+      }
+
+      &::-webkit-scrollbar-thumb:hover {
+        background: #a8a8a8;
+      }
+
+      /* 暂无数据样式 */
+      .no_data {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 14px;
+        color: #909399;
+        text-align: center;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 8px;
+
+        span {
+          margin-top: 23%;
+        }
+      }
+
+      .list_item {
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 6px;
+        padding: 10px;
+        margin-bottom: 10px;
+        cursor: pointer;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        &:hover,
+        &.active {
+          background: rgba(46, 100, 250, 0.1);
+          border-radius: 6px;
+        }
+
+        .item_header {
+          display: flex;
+          align-items: center;
+          margin-bottom: 8px;
+
+          .item_dot {
+            display: inline-block;
+            width: 8px;
+            height: 8px;
+            background: #EE6666;
+            border-radius: 50%;
+            margin-right: 8px;
+          }
+
+          .item_title {
+            font-size: 14px;
+            font-weight: 500;
+            color: #333;
+            flex: 1;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            min-width: 0;
+          }
+
+          .item_tag {
+            color: #FFFFFF;
+            font-size: 12px;
+            padding: 4px 6px;
+            margin-left: 5px;
+            text-align: center;
+            border-radius: 4px;
+            font-weight: 400;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 4px;
+            i:last-child {
+              display: block;
+            }
+          }
+        }
+
+        .item_info {
+          font-size: 12px;
+          color: #999;
+          margin-top: 8px;
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .item_score {
+            text-align: left;
+
+            .score_label {
+              color: #999999;
+            }
+
+            .score_first {
+              color: #F56C6C;
+            }
+
+            .score_separator {
+              color: #999999;
+              margin: 0 4px;
+            }
+
+            .score_second {
+              color: #666666;
+            }
+          }
+
+          .item_exam_count {
+            text-align: right;
+            color: #999999;
+
+            .exam_count {
+              color: #2E64FA;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // vxe表格样式
+  .list_content {
+    border-radius: 10px 10px 10px 10px;
+
+
+    .vxe-table {
+
+      // 展开收起的样式
+      :deep(.el-icon-circle-plus) {
+        color: #DCDFE6 !important;
+        font-size: 16px !important;
+      }
+
+      :deep(.el-icon-remove) {
+        color: #2E64FA !important;
+        font-size: 16px !important;
+      }
+    }
+
+    /* 设置表格行高度和光标样式 */
+    :deep {
+      .vxe-table {
+        border-radius: 8px;
+        overflow-y: auto;
+        overflow-x: hidden;
+
+        // 使用CSS变量设置全局行高
+        --vxe-ui-table-row-height-default: 20px !important;
+
+        // 表头样式
+        .vxe-table--header {
+          display: table-header-group !important;
+          visibility: visible !important;
+          opacity: 1 !important;
+          height: 52px !important;
+        }
+
+        /* 设置表头高度 */
+        .vxe-table--header {
+          .vxe-header--row {
+            height: 52px !important;
+          }
+        }
+
+        // 行样式 - 使用更具体的选择器
+        .vxe-table--body {
+
+          // 表格行样式
+          tbody {
+            tr {
+              border-bottom: 1px solid #F0F2F5;
+              cursor: pointer !important;
+              height: 20px !important;
+              line-height: 20px !important;
+              min-height: 20px !important;
+              max-height: 20px !important;
+
+              &:hover {
+                background-color: #f5f7fa !important;
+              }
+
+              &.row_highlight {
+                background-color: rgba(195, 219, 255, 0.2);
+                cursor: pointer !important;
+              }
+
+              &.selected-row {
+                background-color: rgba(195, 219, 255, 0.2) !important;
+                color: #2E64FA !important;
+                cursor: pointer !important;
+              }
+            }
+          }
+        }
+
+        // 单元格样式,设置内边距为0,确保行高由行高属性控制
+        .vxe-body--column,
+        .vxe-header--column {
+          padding: 0 !important;
+          height: 20px !important;
+          line-height: 20px !important;
+          min-height: 20px !important;
+          max-height: 20px !important;
+        }
+
+        // 表格主体最小高度设置
+        .vxe-table--body {
+          min-height: 0 !important;
+        }
+
+        /* 自定义树形图标样式 */
+        .vxe-tree-icon {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          width: 14px;
+          height: 14px;
+          border-radius: 50%;
+          margin-right: 6px;
+          font-size: 12px;
+          font-weight: bold;
+          line-height: 1;
+        }
+
+        /* 展开状态的节点 */
+        .vxe-tree-icon--minus {
+          background-color: #409EFF;
+          color: white;
+        }
+
+        /* 可展开但未展开的节点 */
+        .vxe-tree-icon--plus {
+          background-color: #C0C4CC;
+          color: white;
+        }
+
+        // 斑马纹
+        .vxe-table--body {
+          tr:nth-child(even) {
+            background-color: #F5F7FA;
+          }
+        }
+      }
+    }
+
+
+    .knowledge_item {
+      display: flex;
+      align-items: center;
+
+      .knowledge_title {
+        font-size: 14px;
+        color: #606266;
+      }
+    }
+
+    .rate_info {
+      // display: flex;
+      align-items: center;
+      gap: 8px;
+      justify-content: center;
+      width: 80px;
+      margin: 0 auto;
+      position: relative;
+    }
+
+    .rate_dot {
+      display: inline-block;
+      width: 6px;
+      height: 6px;
+      border-radius: 50%;
+      position: absolute;
+      top: 8px;
+      left: 2px;
+
+      &.rate_excellent {
+        background-color: #3BA272;
+      }
+
+      &.rate_good {
+        background-color: #FAC858;
+      }
+
+      &.rate_weak {
+        background-color: #EE6666;
+      }
+    }
+
+    .rate_value {
+      font-size: 14px;
+      color: #606266;
+    }
+
+    .diff_value {
+      font-size: 14px;
+      color: #606266;
+
+      &.diff_negative {
+        color: #F56C6C;
+      }
+    }
+  }
+}
+</style>

+ 124 - 0
src/views/analysisReport/studentPage/downloadPdf/components/bookCover.vue

@@ -0,0 +1,124 @@
+<template>
+    <!-- 封面 -->
+    <div :class="['area_page', `${bookBg}`]">
+        <div class="report_cover" v-if="!isCover">
+            <div class="cover_header">
+                <p class="cover_header_title1">
+                    {{ reportTitle }}
+                </p>
+                <p class="cover_header_title1">{{ title }}</p>
+                <p class="cover_header_title2 mt15">({{ subtitle }})</p>
+            </div>
+            <div class="full_screen_button" v-if="showButton">
+                <el-button round :disabled="requestLoading" v-if="requestLoading">点击全屏查看</el-button>
+                <el-button round :loading="openLoading" v-else @click="showFullScreen">点击全屏查看</el-button>
+            </div>
+            <div class="cover_footer">
+                <p class="cover_footer_desc">报告时间:{{ reportTime }}</p>
+                <p class="cover_footer_desc mt5">报告单位:{{ userInfo.schoolName }}</p>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+export default {
+    props: {
+        // 背景
+        bookBg: {
+            type: String,
+            default: () => ''
+        },
+        //标题
+        title: {
+            type: String,
+            default: () => ''
+        },
+        //副标题
+        subtitle: {
+            type: String,
+            default: () => ''
+        },
+        // 是否显示按钮
+        showButton: {
+            type: Boolean,
+            default: () => false
+        },
+        isCover:{//是否是最后一页封面
+            type: Boolean,
+            default: () => false
+        },
+        requestLoading:{
+            type:Boolean,
+            default: false
+        },//接口请求的loading
+        openLoading:{
+            type:Boolean,
+            default: false
+        },//开发全屏预览
+    },
+    data() {
+        return {
+            reportTime: this.$global.getNowDate('yyyy年mm月dd日 hh:mm:ss'),
+            titleSize:32,
+            subtitleSize:24,
+            btnWidth:210,
+            btnHeight:60,
+            btnSize:21,
+            footerSize:13
+        };
+    },
+    computed: {
+        ...mapGetters(["userInfo"]),
+        reportTitle() {
+            return this?.$store?.state?.report?.examSelectItem?.examName ?? '';
+        }
+    },
+    mounted() {
+        // 初始化时执行一次
+        // this.setFontSizeByWindowWidth();
+        // 监听窗口大小变化,实时更新
+        // window.addEventListener('resize', this.setFontSizeByWindowWidth);
+    },
+    beforeDestroy() {
+        // window.removeEventListener('resize', this.setFontSizeByWindowWidth);
+    },
+    methods: {
+        // 计算并设置字体大小的函数
+        // setFontSizeByWindowWidth() {
+        //     const windowWidth = window.innerWidth;
+        //     const windowHeigh = window.innerHeight;
+        //     const scale1 = (30 / 1920).toFixed(2);
+        //     let size1 = Math.round(windowWidth * scale1);
+
+        //     const scale2 = (24 / 1920).toFixed(2);
+        //     let size2 = Math.round(windowWidth * scale2);
+
+        //     const scale3 = (21 / 1920).toFixed(2);
+        //     let size3 = Math.round(windowWidth * scale3);
+
+        //     const btnWidthScale = (210 / 1920).toFixed(2);
+        //     let btnWidth = Math.round(windowWidth * btnWidthScale);
+        //     const btnHeightScale = (60 / 950).toFixed(2);
+        //     let btnHeight = Math.round(windowHeigh * btnHeightScale);
+
+        //     const scale4 = (13 / 1920).toFixed(2);
+        //     let size4 = Math.round(windowWidth * scale4);
+        //     // 限制字体大小范围(最小12px,最大30px)
+        //     this.titleSize = Math.max(12, Math.min(30, size1));
+        //     this.subtitleSize = Math.max(12, Math.min(24, size2));
+        //     this.btnSize = Math.max(12, Math.min(21, size3));
+        //     this.btnWidth = btnWidth;
+        //     this.btnHeight = btnHeight;
+        //     this.footerSize = Math.max(12, Math.min(13, size4));
+        // },
+        //点击进入全屏
+        showFullScreen() {
+            this.$emit('openFullScreen');
+        }
+    }
+};
+</script>
+
+<style scoped lang="scss"></style>

+ 759 - 0
src/views/analysisReport/studentPage/downloadPdf/components/bookFlip.vue

@@ -0,0 +1,759 @@
+<template>
+    <div :class="['report_page', { 'full_screen': isShowFullScreen }]" style="position: absolute;top: -9999999px;z-index: -10;">
+        <!-- 点击封面进入全屏 -->
+        <div class="joint_print_area">
+            <!-- 点击封面进入全屏预览模式 -->
+            <!-- <BookCover :bookBg="`${bookBg}_bg`" :title="coverTitle" :subtitle="subtitle" :showButton="true" :requestLoading="requestLoading" :openLoading="openLoading" @openFullScreen="openFullScreen" :style="{width:`${bookWidth}px`,height:`${bookHeight}px`,margin:'0 auto'}" v-if="!isShowFullScreen"></BookCover> -->
+            <!-- 全屏预览 -->
+            <div id="canvas" v-if="isShowFullScreen">
+                <!-- 右上角放大缩小按钮 -->
+                <div class="close-zoom-icon" @click="CloseFullScreen"><img class="close" src="@/assets/icon/close.webp" /><span>关闭全屏</span></div>
+                <div class="magazine-viewport">
+                    <div class="container">
+                        <!-- 翻书容器 -->
+                        <div class="magazine" id="flipbook" ref="bookContainer">
+                            <!-- 封面 -->
+                            <BookCover :showButton="false" :bookBg="`${bookBg}_bg`" :title="coverTitle" :subtitle="subtitle"></BookCover>
+                            <div class="area_page" v-for="(page, index) in bookPages" :key="index">
+                                <!-- 插槽:自定义每页内容 -->
+                                <slot :page="index + 1" :content="page"></slot>
+                                <!-- <div class="area_page_number">第 {{ currentPage }} 页</div> -->
+                            </div>
+                            <!-- 封面 -->
+                            <BookCover :showButton="false" :isCover="true" :bookBg="`${bookBg}_cover_bg`"></BookCover>
+                            <template v-if="bookPages.length % 2 !== 0">
+                                <BookCover :showButton="false" :isCover="true" :bookBg="`${bookBg}_cover_bg`"></BookCover>
+                            </template>
+                        </div>
+                    </div>
+                    <!-- Next button -->
+                    <div ignore="1" class="next-button"><i class="el-icon-arrow-right"></i></div>
+                    <!-- Previous button -->
+                    <div ignore="1" class="previous-button"><i class="el-icon-arrow-left"></i></div>
+                </div>
+                <!-- 翻页控制按钮 -->
+                <div class="thumbnails">
+                    <div class="book_controls">
+                        <div class="zoom">
+                            <img class="zoom_icon zoom_in_icon" src="@/assets/icon/enlarge_icon.png" />
+                            <img class="zoom_icon zoom_out_icon" src="@/assets/icon/shrink_icon.png" />
+                        </div>
+                        <div class="page_size">
+                            <span class="butn_prev" @click="prevPage"><img src="@/assets/report/turn_prev.webp" /></span>
+                            <span class="page_info">{{ currentPage }}{{ bookPages.length }}</span>
+                            <span class="butn_next" @click="nextPage"><img src="@/assets/report/turn_next.webp" /></span>
+                        </div>
+                        <el-button class="download" type="primary" :loading="loading" @click="DownloadPdf">下载报告册</el-button>
+                    </div>
+                </div>
+                <!-- <div class="page_dialog">
+                    <el-dialog title="下载PDF" class="page_dialog" :visible.sync="showReportLoading" width="500px" top="15%" append-to-body>
+                        <div class="report_loading">
+                            <div class="report_loading_icon">
+                            <img src="@/assets/report/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> -->
+            </div>
+            <!-- 下载模版 -->
+            <!-- style="position: absolute;top: -9999999px;z-index: -10;" -->
+            <div class="magazine-viewport web_mode">
+                <!-- 封面 -->
+                <BookCover class="web_cover web_area_page" :showButton="false" :bookBg="`${bookBg}_bg`" :title="coverTitle" :subtitle="subtitle"></BookCover>
+                <slot type="web_mode"></slot>
+                <!-- 封面 -->
+                <BookCover class="web_cover web_area_page" :showButton="false" :isCover="true" :bookBg="`${bookBg}_cover_bg`"></BookCover>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+// import $ from 'jquery';  // 先导入 jQuery
+// import 'turn.js'; 
+import turn from '@/utils/turn.js';
+import zoom from './js/zoom.min.js';
+import BookCover from './bookCover.vue';
+import { disableControls, addPage,largeMagazineWidth,resizeViewport,setArrows,loadLargePage,loadSmallPage } from './js/magazine.js';
+import { jsPDF } from "jspdf";
+import html2canvas from "html2canvas";
+import domtoimage from 'dom-to-image';
+import { mapGetters } from "vuex";
+export default {
+    props: {
+        // 书页数据(数组长度建议为偶数)
+        bookPages: {
+            type: Array,
+            required: true,
+            default: () => []
+        },
+        bookBg:{
+            type:String,
+            default: 'grade_leader'
+        },
+        coverTitle:{//封面标题
+            type:String,
+            default: '联校总分分析报告册'
+        },
+        subtitle:{//封面副标题
+            type:String,
+            default: '联校总分分析报告册'
+        },
+        requestLoading:{
+            type:Boolean,
+            default: false
+        },//接口请求的loading
+        openLoading:{
+            type:Boolean,
+            default: false
+        },//开发全屏预览
+        
+    },
+    components:{BookCover},
+    data() {
+        return {
+            isShowFullScreen: false,
+            bookHeight:780,//封面高度
+            bookWidth:600,//封面宽度
+            currentPage: '', // 当前页码
+            totalPages: 0, // 总页数(由 turn.js 计算)
+            showReportLoading:false,
+            targetProgress:0,
+            loading:false
+        };
+    },
+    computed: {
+        ...mapGetters(["userInfo"]),
+        reportTitle() {
+            return this?.$store?.state?.report?.examSelectItem?.examName ?? '';
+        }
+    },
+    mounted() {
+        //设置书本宽度和高度
+        this.setBookSize();
+        // 监听窗口大小变化,实时更新
+        window.addEventListener('resize', this.setBookSize);
+    },
+    beforeDestroy() {
+        // 销毁翻书实例,避免内存泄漏
+        if (this.$refs.bookContainer) {
+            $(this.$refs.bookContainer).turn('destroy');
+        }
+        window.removeEventListener('resize', this.setBookSize);
+    },
+    methods: {
+        //设置书本宽度和高度
+        setBookSize(){
+            const bookHeight = $(window).height() - 120;//书本高度
+            this.bookHeight = bookHeight;
+            this.bookWidth = Math.round(bookHeight * 0.706);// 书本宽度 0.706 宽高的比例
+        },
+        //点击查看全屏
+        openFullScreen(){
+            this.$emit('OpenBookImages');
+        },
+        //打开全屏预览
+        showFullScreen() {
+            $('#canvas').hide();
+            this.isShowFullScreen = true;//是否显示全屏
+            this.initBook(); // 初始化翻书效果
+        },
+        // 初始化翻书效果
+        initBook() {
+            const b_height = $(window).height() - 120;//书本高度
+            const b_width = Math.round(b_height * 0.706) * 2;// 书本宽度 0.706 宽高的比例
+            this.$nextTick(() => {
+                $('#canvas').fadeIn(1000);
+                const $book = $(this.$refs.bookContainer);
+                if ($book.width() == 0 || $book.height() == 0) {
+                    setTimeout(loadApp, 10);
+                    return;
+                }
+                // 初始化 turn.js
+                const _that = this;
+                const totalPages = this.bookPages.length;//总页数
+                $book.turn({
+                    // 页面尺寸(A4 高度示例)
+                    // height: 1285, // 单页高度(对应 A4 72dpi 高度)
+                    // width: 1816, // 单页宽度(对应 A4 72dpi 宽度)908
+                    width: b_width, // 书本宽度
+                    height: b_height, // 书本高度
+                    duration: 1000,   //翻页速度,值越小越快
+                    autoCenter: true, // 自动居中
+                    acceleration: false,//设置硬件加速模式,用于触摸设备此值必须是真的
+                    gradients: true,//置翻页时是否显示翻页跟阴影
+                    display: 'double', // 双页显示(适合PC端)
+                    elevation: 50, // 翻页阴影高度(增强立体感)
+                    pages: totalPages,
+                    when: {
+                        // 翻页前触发
+                        turning: function (event, page, view) {
+                            const book = $(this);
+                            const pages = book.turn('pages') - 2;
+                            if(page > 1 && page < pages){
+                                console.log(page , pages)
+                                const prev = view[0] - 1;
+                                const next = view[1] < pages ? view[1] - 1 : '';
+                                _that.currentPage = next?`${prev}-${next}/`:`${prev}/`;
+                            }else{
+                                _that.currentPage = '';
+                            }
+                            // 显示和隐藏按钮
+                            disableControls(page);
+                        },
+                        // 翻页后触发
+                        turned: function (event, page, view) {
+                            // 显示和隐藏按钮
+                            disableControls(page);
+                            $(this).turn('center');
+                            if (page == 1) {
+                                // 表示调用翻页 API 并指定翻页效果为 “Peel( peeling,即 “剥开” 效果)”,且翻页的起始位置为右下角(br 是 bottom right 的缩写)
+                                $(this).turn('peel', 'br');
+                            }
+                        },
+                        // missing: function (event, pages) {
+                        // 	// Add pages that aren't in the magazine
+                        // 	for (var i = 0; i < pages.length; i++){
+                        //         addPage(pages[i], $(this));
+                        //     }
+                        // }
+                    }
+                });
+                // 初始化页码信息
+                // this.totalPages = $book.turn('pages');
+                // Zoom.js
+
+                $('#canvas .magazine-viewport').zoom({
+
+                    flipbook: $('.magazine'),
+
+                    max: function () {
+
+                        return largeMagazineWidth() / $('.magazine').width();
+
+                    },
+
+                    when: {
+
+                        swipeLeft: function () {
+
+                            $(this).zoom('flipbook').turn('next');
+
+                        },
+
+                        swipeRight: function () {
+
+                            $(this).zoom('flipbook').turn('previous');
+
+                        },
+
+
+
+                        resize: function (event, scale, page, pageElement) {
+
+                            // if (scale == 1)
+
+                            //     loadSmallPage(page, pageElement);
+
+                            // else
+
+                            //     loadLargePage(page, pageElement);
+
+                        },
+
+                        zoomIn: function () {
+
+                            $('.made').hide();
+
+                            $('.magazine').removeClass('animated').addClass('zoom-in');
+
+                            $('.zoom-icon').removeClass('zoom-icon-in').addClass('zoom-icon-out');
+
+                            // if (!window.escTip && !$.isTouch) {
+
+                            //     escTip = true;
+
+                            //     $('<div />', { 'class': 'exit-message' }).
+
+                            //         html('<div>Press ESC to exit</div>').
+
+                            //         appendTo($('body')).
+
+                            //         delay(2000).
+
+                            //         animate({ opacity: 0 }, 500, function () {
+
+                            //             $(this).remove();
+
+                            //         });
+
+                            // }
+
+                        },
+
+                        zoomOut: function () {
+
+                            $('.exit-message').hide();
+
+                            $('.thumbnails').fadeIn();
+
+                            $('.made').fadeIn();
+
+                            $('.zoom-icon').removeClass('zoom-icon-out').addClass('zoom-icon-in');
+
+                            setTimeout(function () {
+
+                                $('.magazine').addClass('animated').removeClass('zoom-in');
+
+                                resizeViewport();
+
+                            }, 0);
+
+                        }
+
+                    }
+
+                });
+
+
+                $(document).keydown(function (e) {
+
+                    const previous = 37, next = 39, esc = 27;
+
+                    switch (e.keyCode) {
+
+                        case previous:
+
+                            // left arrow
+
+                            $('.magazine').turn('previous');
+
+                            e.preventDefault();
+
+                            break;
+
+                        case next:
+
+                            //right arrow
+
+                            $('.magazine').turn('next');
+
+                            e.preventDefault();
+
+                            break;
+
+                        case esc:
+
+                            $('#canvas .magazine-viewport').zoom('zoomOut');
+
+                            e.preventDefault();
+
+                            break;
+
+                    }
+
+                });
+
+
+                $(window).resize(function () {
+
+                    resizeViewport();
+
+                }).bind('orientationchange', function () {
+
+                    resizeViewport();
+
+                });
+                // Events for the next button
+
+                $('.next-button').click(function () {
+
+                    $('.magazine').turn('next');
+
+                    setTimeout(function () {
+
+                        setArrows();
+
+                    }, 300);
+
+                });
+
+
+
+                // Events for the next button	
+
+                $('.previous-button').bind($.mouseEvents.over, function () {
+
+                    $(this).addClass('previous-button-hover');
+
+                }).bind($.mouseEvents.out, function () {
+
+                    $(this).removeClass('previous-button-hover');
+
+                }).bind($.mouseEvents.down, function () {
+
+                    $(this).addClass('previous-button-down');
+
+                }).bind($.mouseEvents.up, function () {
+
+                    $(this).removeClass('previous-button-down');
+
+                }).click(function () {
+
+                    $('.magazine').turn('previous');
+
+                    setTimeout(function () {
+
+                        setArrows();
+
+                    }, 300);
+
+                });
+                // Zoom icon
+                $('.zoom_in_icon').on('click', function () {
+                    $('#canvas .magazine-viewport').zoom('zoomIn');
+                });
+                $('.zoom_out_icon').on('click', function () {
+                    $('#canvas .magazine-viewport').zoom('zoomOut');
+                });
+                resizeViewport();
+
+                $('.magazine').addClass('animated');
+            })
+        },
+        //退出全屏
+        CloseFullScreen() {
+            this.isShowFullScreen = false;
+        },
+        // 上一页
+        prevPage() {
+            $('.magazine').turn('previous');
+            setTimeout(function () {
+                setArrows();
+            }, 300);
+        },
+        // 下一页
+        nextPage() {
+            $('.magazine').turn('next');
+            setTimeout(function () {
+                setArrows();
+            }, 300);
+        },
+        async DownloadPdf() {
+            this.loading = true;
+            this.targetProgress = 1;
+            // this.showReportLoading = true;
+            //获取所有的area_page 元素
+            const elements = document.querySelectorAll(".web_mode .web_cover");
+            const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
+            const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
+            const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
+            let yPos = 0; //当前图像在pdf页面上的垂直位置的变量
+            let i = 1;
+            let imageList = [];
+            // 遍历每个 area_page 元素并按顺序处理
+            for (const element of elements) {
+                await html2canvas(element, {
+                    scale: 2, // 增加缩放比例
+                    useCORS: true, // 允许跨域
+                    logging: true, // 是否显示进度条
+                    letterRendering: true, // 文字抗锯齿 启用字母渲染
+                }).then(async(canvas) => {
+                    // 将 Canvas 对象转换为 PNG 格式的 Data URL 字符串
+                    const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
+                    const imgUrl = URL.createObjectURL(blob);
+                    imageList.push(imgUrl)
+                });
+            }
+            imageList.splice(1, 0, ...this.bookPages);
+            // console.log(imageList)
+            const stepLens = imageList?.length || 1;
+            for (const imgData of imageList) {
+                const imgProps = pdf.getImageProperties(imgData); // 获取图像的属性,包括宽度和高度
+                const imgWidth = imgProps.width;
+                const imgHeight = imgProps.height;
+                const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight); //计算图片的缩放比例
+                const adjustWidth = imgWidth * ratio;
+                const adjustHeight = imgHeight * ratio;
+
+                // 在添加每个图像之前,检查当前页面的高度是否足够。如果不够,则添加新页面,并将 yPos 重置为 0
+                if (yPos + adjustHeight > pdfHeight) {
+                    pdf.addPage();
+                    yPos = 0;
+                }
+
+                // 将图像添加到pdf中
+                pdf.addImage(imgData, "PNG", 0, yPos, adjustWidth, adjustHeight);
+                yPos += adjustHeight;
+
+                // 如果添加图像后剩余空间不足一页,则添加新页面
+                if (yPos > pdfHeight) {
+                    pdf.addPage();
+                    yPos = 0;
+                }
+                const currentProgress  = Math.min(Math.round((i / stepLens) * 100),100);
+                this.targetProgress = currentProgress;
+                i++;
+            }
+            // 保存pdf文件
+            pdf.save(`${this.reportTitle}_${this.coverTitle}.pdf`);
+            // this.showReportLoading = false; //关闭加载loading
+            this.loading = false;
+        },
+        // async DownloadPdfNew() {
+        //     //获取所有的area_page 元素
+        //     const elements = document.querySelectorAll(".web_mode .web_area_page");
+        //     this.loading = true;
+        //     // this.targetProgress = 1;
+        //     // this.showReportLoading = true;
+        //     const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
+        //     const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
+        //     const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
+        //     let yPos = 0; //当前图像在pdf页面上的垂直位置的变量
+        //     // let i = 1;
+        //     let imageList = [];
+        //     // 遍历每个 area_page 元素并按顺序处理
+        //     for (const element of elements) {
+        //         await html2canvas(element, {
+        //             scale: 2, // 增加缩放比例
+        //             useCORS: true, // 允许跨域
+        //             logging: true, // 是否显示进度条
+        //             letterRendering: true, // 文字抗锯齿 启用字母渲染
+        //         }).then(async(canvas) => {
+        //             // 将 Canvas 对象转换为 PNG 格式的 Data URL 字符串
+        //             const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
+        //             const imgUrl = URL.createObjectURL(blob);
+        //             imageList.push(imgUrl)
+        //         });
+        //     }
+        //     // const stepLens = imageList?.length || 1;
+        //     for (const imgData of imageList) {
+        //         const imgProps = pdf.getImageProperties(imgData); // 获取图像的属性,包括宽度和高度
+        //         const imgWidth = imgProps.width;
+        //         const imgHeight = imgProps.height;
+        //         const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight); //计算图片的缩放比例
+        //         const adjustWidth = imgWidth * ratio;
+        //         const adjustHeight = imgHeight * ratio;
+
+        //         // 在添加每个图像之前,检查当前页面的高度是否足够。如果不够,则添加新页面,并将 yPos 重置为 0
+        //         if (yPos + adjustHeight > pdfHeight) {
+        //             pdf.addPage();
+        //             yPos = 0;
+        //         }
+
+        //         // 将图像添加到pdf中
+        //         pdf.addImage(imgData, "PNG", 0, yPos, adjustWidth, adjustHeight);
+        //         yPos += adjustHeight;
+
+        //         // 如果添加图像后剩余空间不足一页,则添加新页面
+        //         if (yPos > pdfHeight) {
+        //             pdf.addPage();
+        //             yPos = 0;
+        //         }
+        //         // const currentProgress  = Math.min(Math.round((i / stepLens) * 100),100);
+        //         // this.targetProgress = currentProgress;
+        //         // i++;
+        //     }
+        //     // 保存pdf文件
+        //     pdf.save(`${this.reportTitle}_${this.coverTitle}.pdf`);
+        //     // this.showReportLoading = false; //关闭加载loading
+        //     this.loading = false;
+        //     this.$emit('PdfLoadEnd');
+        // },
+        async DownloadPdfNew() {
+            //获取所有的area_page 元素
+            const elements = document.querySelectorAll(".web_mode .web_area_page");
+            const elLens = elements.length - 1;
+            this.loading = true;
+            // this.targetProgress = 1;
+            // this.showReportLoading = true;
+            const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
+            const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
+            const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
+            // 遍历每个 area_page 元素并按顺序处理
+            for (const [pageIndex, element] of Array.from(elements).entries()) {
+                // 确保页面元素存在并可见
+                await this.$nextTick();
+                if (element) {
+                    // 确保元素可见并已渲染
+                    // 确保元素可见并已渲染,并设置为横向排列样式
+                    element.style.visibility = 'visible';
+                    element.offsetHeight; // 触发重排
+                    // 等待元素完全渲染
+                    await new Promise(resolve => setTimeout(resolve, 100));
+                    
+                    // 使用 dom-to-image 生成图片
+                    const dataUrl = await domtoimage.toJpeg(element, {
+                        quality: 0.8,// 降低质量以减小文件大小
+                        // 获取完整尺寸
+                        width: element.scrollWidth*2,
+                        height: element.scrollHeight*2,
+                        // 提高画布分辨率以获得更清晰的图像
+                        // canvasWidth: element.scrollWidth * 2,
+                        // canvasHeight: element.scrollHeight * 2,
+                        // 新增高清配置
+                        // pixelRatio: window.devicePixelRatio * 2,
+                        style: {
+                        'transform': 'scale(2)',
+                        'transform-origin': 'left top',
+                        'background-color': '#ffffff',
+                        // 关键:背景图尺寸适配原元素
+                        'background-size': `${element.scrollWidth}px ${element.scrollHeight}px`, // 优先铺满且完整显示
+                        'background-position': 'top left',
+                        'background-repeat': 'no-repeat', // 防止背景图重复导致视觉不完整
+                        '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;
+                    // console.log("打印pdf图片尺寸", imgWidth, imgHeight);
+                    // console.log("打印pdf页面尺寸", pdfWidth, pdfHeight);
+                    // console.log("打印pdf缩放比例", ratio);
+                    // console.log("打印pdf缩放后尺寸",adjustWidth, adjustHeight);
+                    // console.log("打印pdf图片位置", xPosition, yPosition);
+                    // 如果不是第一页,添加新页面
+                    if (pageIndex > 0) 
+                    {
+                        pdf.addPage();
+                    }
+                    //处理封面图铺满屏
+                    let pdfAdjustHeight = adjustHeight;
+                    if(pageIndex == 0 || pageIndex == elLens){
+                        pdfAdjustHeight = adjustHeight + 6
+                    }
+                    pdf.addImage(dataUrl, "JPEG", xPosition, yPosition - 6, adjustWidth, pdfAdjustHeight);
+                }
+            }
+            // 保存pdf文件
+            pdf.save(`${this.reportTitle}_${this.coverTitle}.pdf`);
+            // this.showReportLoading = false; //关闭加载loading
+            this.loading = false;
+            this.$emit('PdfLoadEnd');
+        },
+    }
+};
+</script>
+
+<style scoped lang="scss">
+@import '../components/css/magazine.css'; // 引入 SCSS 文件
+
+.report_page {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 0;
+    box-sizing: border-box;
+    background-color: #F0F4FB;
+}
+
+/* 书本容器样式 */
+// #flipbook {
+//     box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
+// }
+
+/* 单页样式(注意:宽度为书本的一半,因为默认双页显示) */
+// .joint_print_area {
+//     width: 50%;
+//     height: 100%;
+//     background-color: #fff;
+//     overflow: hidden;
+// }
+
+// .area_page {
+//     width: 100%;
+//     height: 100%;
+//     padding: 32px;
+//     box-sizing: border-box;
+//     // background: #FFFFFF;
+//     box-shadow: inset 1px 0px 0px 0px #EEEEEE;
+// }
+
+/* 控制按钮样式 */
+.book_controls {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    height: 60px;
+    background: #FFFFFF;
+    // margin-top: 40px;
+    padding: 12px 24px;
+    box-sizing: border-box;
+
+    .zoom {
+        display: inline-flex;
+        align-items: center;
+
+        .zoom_icon {
+            width: 32px;
+            height: 32px;
+            margin-right: 20px;
+            cursor: pointer;
+        }
+    }
+
+    .page_size {
+        display: inline-flex;
+        align-items: center;
+        .butn_prev{
+            width: 24px;
+            height: 24px;
+            margin-right: 15px;
+            cursor: pointer;
+        }
+        .butn_next{
+            width: 24px;
+            height: 24px;
+            margin-left: 15px;
+            cursor: pointer;
+        }
+        img{
+            width: 24px;
+            height: 24px;
+        }
+        .page_info {
+            font-weight: 500;
+            font-size: 16px;
+            color: #333333;
+        }
+    }
+
+    .download {
+        padding: 8px 16px;
+        height: 36px;
+        background-color: #2E64FA;
+        border-radius: 4px;
+        font-size: 14px;
+        color: #FFFFFF;
+        border: none;
+        cursor: pointer;
+
+        &:disabled {
+            background-color: #ccc;
+            cursor: not-allowed;
+        }
+    }
+}
+</style>

+ 262 - 0
src/views/analysisReport/studentPage/downloadPdf/components/css/magazine.css

@@ -0,0 +1,262 @@
+/* body {
+	overflow: hidden;
+	background-color: #0c0c0c;
+	margin: 0;
+	padding: 0;
+} */
+#canvas {
+	/* position: absolute; */
+	width: 100%;
+	height: 100%;
+}
+.magazine-viewport .container {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	width: 1816px;
+	height: 1285px;
+	margin: auto;
+}
+.magazine-viewport .magazine {
+	width: 1816px;
+	height: 1285px;
+	left: -661px;
+	top: -300px;
+}
+.magazine-viewport .page {
+	width: 908px;
+	height: 1285px;
+	background-color: white;
+	/* background-repeat: no-repeat;
+	background-size: 100% 100%; */
+}
+.magazine-viewport .zoomer .region {
+	display: none;
+}
+.magazine .region {
+	position: absolute;
+	overflow: hidden;
+	background: #0066FF;
+	opacity: 0.2;
+	-webkit-border-radius: 10px;
+	-moz-border-radius: 10px;
+	-ms-border-radius: 10px;
+	-o-border-radius: 10px;
+	border-radius: 10px;
+	cursor: pointer;
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
+	filter: alpha(opacity=20);
+}
+.magazine .region:hover {
+	opacity: 0.5;
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+	filter: alpha(opacity=50);
+}
+.magazine .region.zoom {
+	opacity: 0.01;
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=1)";
+	filter: alpha(opacity=1);
+}
+.magazine .region.zoom:hover {
+	opacity: 0.2;
+	-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
+	filter: alpha(opacity=20);
+}
+.magazine .page {
+	-webkit-box-shadow: 0 0 20px rgba(0,0,0,0.2);
+	-moz-box-shadow: 0 0 20px rgba(0,0,0,0.2);
+	-ms-box-shadow: 0 0 20px rgba(0,0,0,0.2);
+	-o-box-shadow: 0 0 20px rgba(0,0,0,0.2);
+	box-shadow: 0 0 20px rgba(0,0,0,0.2);
+}
+.magazine-viewport .page img {
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	-khtml-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+	margin: 0;
+}
+.magazine .even .gradient {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	/* padding: 30px; */
+	box-sizing: border-box;
+	background: -webkit-gradient(linear, left top, right top, color-stop(0.95, rgba(255,255,255,0)), color-stop(1, rgba(0,0,0,0.05)));
+	background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 95%, rgba(0,0,0,0.05) 100%);
+	background-image: -moz-linear-gradient(left, rgba(255,255,255,0) 95%, rgba(0,0,0,0.05) 100%);
+	background-image: -ms-linear-gradient(left, rgba(255,255,255,0) 95%, rgba(0,0,0,0.05) 100%);
+	background-image: -o-linear-gradient(left, rgba(255,255,255,0) 95%, rgba(0,0,0,0.05) 100%);
+	background-image: linear-gradient(left, rgba(255,255,255,0) 95%, rgba(0,0,0,0.05) 100%);
+}
+.magazine .odd .gradient {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	/* padding: 30px; */
+	box-sizing: border-box;
+	background: -webkit-gradient(linear, right top, left top, color-stop(0.95, rgba(255,255,255,0)), color-stop(1, rgba(0,0,0,0.02)));
+	background-image: -webkit-linear-gradient(right, rgba(255,255,255,0) 95%, rgba(0,0,0,0.02) 100%);
+	background-image: -moz-linear-gradient(right, rgba(255,255,255,0) 95%, rgba(0,0,0,0.02) 100%);
+	background-image: -ms-linear-gradient(right, rgba(255,255,255,0) 95%, rgba0,0,0,0.02 100%);
+	background-image: -o-linear-gradient(right, rgba(255,255,255,0) 95%, rgba(0,0,0,0.02) 100%);
+	background-image: linear-gradient(right, rgba(255,255,255,0) 95%, rgba(0,0,0,0.02) 100%);
+}
+.magazine-viewport .zoom-in .even .gradient, .zoom-in .odd .gradient {
+	display: none;
+}
+.magazine-viewport .loader {
+	background-image: url(../image/loader.gif);
+	width: 22px;
+	height: 22px;
+	position: absolute;
+	top: 280px;
+	left: 219px;
+}
+.magazine-viewport .shadow {
+	-webkit-transition: -webkit-box-shadow 0.5s;
+	-moz-transition: -moz-box-shadow 0.5s;
+	-o-transition: -webkit-box-shadow 0.5s;
+	-ms-transition: -ms-box-shadow 0.5s;
+	-webkit-box-shadow: 0 0 20px #ccc;
+	-moz-box-shadow: 0 0 20px #ccc;
+	-o-box-shadow: 0 0 20px #ccc;
+	-ms-box-shadow: 0 0 20px #ccc;
+	box-shadow: 0 0 20px #ccc;
+}
+.next-button {
+ 	width: 59px;
+	height: 59px;
+	position: absolute;
+	top: 50%;
+	/* background:url('../image/arrows.png') -59px 0; */
+	right: 10px;
+	z-index:10;
+	cursor: pointer;
+}
+.next-button .el-icon-arrow-right,.previous-button .el-icon-arrow-left{
+	font-size: 55px;
+	color: #999999;
+}
+.previous-button {
+	width: 59px;
+	height: 59px;
+	position: absolute;
+	top: 50%;
+	/* background-image: url('../image/arrows.png'); */
+	z-index:10;
+	cursor: pointer;
+}
+/* .magazine-viewport .previous-button-hover {
+	background-position: 0 -59px;
+	cursor: pointer;
+}
+.magazine-viewport .next-button-hover {
+	background-position: -59px -59px;
+	cursor: pointer;
+} */
+.magazine-viewport .zoom-in .next-button, .magazine-viewport .zoom-in .previous-button {
+	display: none;
+}
+.animated {
+	-webkit-transition: margin-left 0.5s;
+	-moz-transition: margin-left 0.5s;
+	-ms-transition: margin-left 0.5s;
+	-o-transition: margin-left 0.5s;
+	transition: margin-left 0.5s;
+}
+.thumbnails{
+	position:absolute;
+	bottom:0;
+	left:0;
+	width:100%;
+	height:140px;
+	z-index:1;
+	display: flex;
+	align-items: flex-end;
+}
+.exit-message {
+	position: absolute;
+	top: 10px;
+	left: 0;
+	width: 100%;
+	height: 40px;
+	z-index: 10000;
+}
+.exit-message > div {
+	width: 140px;
+	height: 30px;
+	margin: auto;
+	background: rgba(0,0,0,0.5);
+	text-align: center;
+	font: 12px arial;
+	line-height: 30px;
+	color: white;
+	-webkit-border-radius: 10px;
+	-moz-border-radius: 10px;
+	-ms-border-radius: 10px;
+	-o-border-radius: 10px;
+	border-radius: 10px;
+}
+/* .zoom-icon {
+	position: absolute;
+	z-index: 1000;
+	width: 22px;
+	height: 22px;
+	top: 10px;
+	right: 10px;
+	background-image: url(../image/zoom-icons.png);
+	background-size: 88px 22px;
+}
+.zoom-icon-in {
+	background-position: 0 0;
+	cursor: pointer;
+}
+.zoom-icon-in.zoom-icon-in-hover {
+	background-position: -22px 0;
+	cursor: pointer;
+}
+.zoom-icon-out {
+	background-position: -44px 0;
+}
+.zoom-icon-out.zoom-icon-out-hover {
+	background-position: -66px 0;
+	cursor: pointer;
+} */
+.close-zoom-icon{
+	width: 100px;
+	height: 36px;
+	background: #FFFFFF;
+	border-radius: 4px;
+	border: 1px solid #DCDFE6;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+	position: absolute;
+	top: 40px;
+	right: 24px;
+	z-index: 10;
+}
+.close-zoom-icon .close{
+	width: 20px;
+	height: 20px;
+	margin-right: 6px;
+}
+.close-zoom-icon span{
+	font-weight: 400;
+	font-size: 14px;
+	color: #666666;
+}
+/* .bottom {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+} */

BIN
src/views/analysisReport/studentPage/downloadPdf/components/image/loader.gif


+ 325 - 0
src/views/analysisReport/studentPage/downloadPdf/components/js/magazine.js

@@ -0,0 +1,325 @@
+/*
+ * Magazine sample
+ */
+
+export function setArrows() {
+    /*var width = $(window).width();
+  
+      //alert("chushihua"+width);		
+      var height = $(window).height();
+      var bookWidth = $(".magazine").width();
+      var bookHeight = $(".magazine").height();
+      //alert("chushihua"+bookWidth);		
+      var arrowSize = $(".next-button").width();
+      //alert(arrowSize);
+     alert($(".magazine").offset().left+"\n"+$('.next-button').offset().left);
+  
+      var LeftArrowLeft = - ( width - bookWidth ) / 4 + 'px' ;
+      //alert(LeftArrowLeft);
+      var RightArrowLeft = - ( width - bookWidth+ arrowSize*2) / 4 + 'px' ;
+      //alert(RightArrowLeft); 
+      //alert(RightArrowLeft);
+          //alert("zhihou"+bookWidth);	
+      	
+      $('.next-button').css( "right",RightArrowLeft );
+      $('.previous-button').css( "left", LeftArrowLeft );*/
+    setTimeout(function () {
+        var width = $(window).width();
+        var bookWidth = $(".magazine").width();
+        var arrowSize = $(".next-button").width();
+        var magaLeft = $(".magazine").offset().left;
+        var nextLeft = (width - bookWidth - magaLeft - 60) / 2;
+        //alert("width "+width +"\nbookWidth :"+bookWidth +"\nmagaLeft:"+magaLeft+"\nnextLeft:"+nextLeft);
+        $(".next-button").animate({ right: nextLeft }, 300);
+
+        $(".previous-button").animate({ left: nextLeft }, 300);
+    }, 100);
+}
+
+export  function addPage(page, book) {
+    var id,pages = book.turn("pages");
+
+    // Create a new element for this page
+    var element = $("<div />", {});
+
+    // Add the page to the flipbook
+    if (book.turn("addPage", element, page)) {
+        // Add the initial HTML
+        // It will contain a loader indicator and a gradient
+        element.html('<div class="gradient"></div><div class="loader"></div>');
+
+        // Load the page
+        console.log(page, element,99999)
+        loadPage(page, element);
+    }
+}
+
+function loadPage(page, pageElement) {
+    // Create an image element
+    var img = $("<img />");
+
+    img.mousedown(function (e) {
+        e.preventDefault();
+    });
+
+    img.load(function () {
+        // Set the size
+        $(this).css({
+            width: "100%",
+            height: "100%",
+        });
+
+        // Add the image to the page after loaded
+        $(this).appendTo(pageElement);
+
+        // Remove the loader indicator
+        pageElement.find(".loader").remove();
+    });
+
+    // Load the page
+    img.attr("src", "pages/" + page + ".png");
+
+    loadRegions(page, pageElement);
+}
+
+// Zoom in / Zoom out
+export function zoomTo(event) {
+    setTimeout(function () {
+        if ($("#canvas .magazine-viewport").data().regionClicked) {
+            $("#canvas .magazine-viewport").data().regionClicked = false;
+        } else {
+            if ($("#canvas .magazine-viewport").zoom("value") == 1) {
+                $("#canvas .magazine-viewport").zoom("zoomIn", event);
+            } else {
+                $("#canvas .magazine-viewport").zoom("zoomOut");
+            }
+        }
+    }, 1);
+}
+
+// Load regions
+export function loadRegions(page, element) {
+    $.getJSON("pages/" + page + "-regions.json").done(function (data) {
+        $.each(data, function (key, region) {
+            addRegion(region, element);
+        });
+    });
+}
+
+// Add region
+export function addRegion(region, pageElement) {
+    var reg = $("<div />", {
+        class: "region  " + region["class"],
+    }),
+        options = $(".magazine").turn("options"),
+        pageWidth = options.width / 2,
+        pageHeight = options.height;
+
+    reg.css({
+        top: Math.round((region.y / pageHeight) * 100) + "%",
+        left: Math.round((region.x / pageWidth) * 100) + "%",
+        width: Math.round((region.width / pageWidth) * 100) + "%",
+        height: Math.round((region.height / pageHeight) * 100) + "%",
+    }).attr("region-data", $.param(region.data || ""));
+
+    reg.appendTo(pageElement);
+}
+
+// Process click on a region
+export function regionClick(event) {
+    var region = $(event.target);
+
+    if (region.hasClass("region")) {
+        $("#canvas .magazine-viewport").data().regionClicked = true;
+
+        setTimeout(function () {
+            $("#canvas .magazine-viewport").data().regionClicked = false;
+        }, 100);
+
+        var regionType = $.trim(region.attr("class").replace("region", ""));
+
+        return processRegion(region, regionType);
+    }
+}
+
+// Process the data of every region
+export function processRegion(region, regionType) {
+    data = decodeParams(region.attr("region-data"));
+
+    switch (regionType) {
+        case "link":
+            window.open(data.url);
+
+            break;
+        case "zoom":
+            var regionOffset = region.offset(),
+                viewportOffset = $("#canvas .magazine-viewport").offset(),
+                pos = {
+                    x: regionOffset.left - viewportOffset.left,
+                    y: regionOffset.top - viewportOffset.top,
+                };
+
+            $("#canvas .magazine-viewport").zoom("zoomIn", pos);
+
+            break;
+        case "to-page":
+            $(".magazine").turn("page", data.page);
+            break;
+    }
+}
+
+// Load large page
+export function loadLargePage(page, pageElement) {
+    var img = $("<img />");
+
+    img.load(function () {
+        var prevImg = pageElement.find("img");
+        $(this).css({
+            width: "100%",
+            height: "100%",
+        });
+        $(this).appendTo(pageElement);
+        prevImg.remove();
+    });
+
+    // Loadnew page
+    img.attr("src", "pages/" + page + "-large.png");
+}
+
+// Load small page
+export function loadSmallPage(page, pageElement) {
+    var img = pageElement.find("img");
+
+    img.css({
+        width: "100%",
+        height: "100%",
+    });
+
+    img.unbind("load");
+    // Loadnew page
+    img.attr("src", "pages/" + page + ".png");
+}
+
+// http://code.google.com/p/chromium/issues/detail?id=128488
+export function isChrome() {
+    return navigator.userAgent.indexOf("Chrome") != -1;
+}
+
+export function disableControls(page) {
+    if (page == 1) $(".previous-button").hide();
+    else $(".previous-button").show();
+
+    if (page == $(".magazine").turn("pages")) $(".next-button").hide();
+    else $(".next-button").show();
+}
+
+// Set the width and height for the viewport
+export function resizeViewport() {
+    var width = $(window).width(),
+        height = $(window).height(),
+        options = $(".magazine").turn("options");
+
+    $(".magazine").removeClass("animated");
+
+    $("#canvas .magazine-viewport")
+        .css({
+            width: width,
+            height: height,
+        })
+        .zoom("resize");
+    setArrows();
+
+    if ($(".magazine").turn("zoom") == 1) {
+        var bound = calculateBound({
+            width: options.width,
+            height: options.height,
+            boundWidth: Math.min(options.width, width),
+            boundHeight: Math.min(options.height, height),
+        });
+
+        if (bound.width % 2 !== 0) bound.width -= 1;
+
+        if (
+            bound.width != $(".magazine").width() ||
+            bound.height != $(".magazine").height()
+        ) {
+            $(".magazine").turn("size", bound.width, bound.height);
+
+            if ($(".magazine").turn("page") == 1) $(".magazine").turn("peel", "br");
+        }
+
+        $(".magazine").css({
+            top: -bound.height / 2 - 30,
+            left: -bound.width / 2,
+        });
+    }
+    var magazineOffset = $(".magazine").offset(),
+        boundH = height - magazineOffset.top - $(".magazine").height(),
+        marginTop = (boundH - $(".thumbnails > div").height()) / 2;
+    if (marginTop < 0) {
+        $(".thumbnails").css({
+            height: 1,
+        });
+    } else {
+        $(".thumbnails").css({
+            height: boundH,
+        });
+        // $(".thumbnails > div").css({
+        //     marginTop: marginTop,
+        // });
+    }
+
+    if (magazineOffset.top < $(".made").height()) $(".made").hide();
+    else $(".made").show();
+
+    $(".magazine").addClass("animated");
+}
+
+// Number of views in a flipbook
+export function numberOfViews(book) {
+    return book.turn("pages") / 2 + 1;
+}
+
+// Current view in a flipbook
+export function getViewNumber(book, page) {
+    return parseInt((page || book.turn("page")) / 2 + 1, 10);
+}
+
+// Width of the flipbook when zoomed in
+export function largeMagazineWidth() {
+    return 2214;
+}
+
+// decode URL Parameters
+export function decodeParams(data) {
+    var parts = data.split("&"),
+        d,
+        obj = {};
+    for (var i = 0; i < parts.length; i++) {
+        d = parts[i].split("=");
+        obj[decodeURIComponent(d[0])] = decodeURIComponent(d[1]);
+    }
+    return obj;
+}
+
+// Calculate the width and height of a square within another square
+export function calculateBound(d) {
+    var bound = {
+        width: d.width,
+        height: d.height,
+    };
+    if (bound.width > d.boundWidth || bound.height > d.boundHeight) {
+        var rel = bound.width / bound.height;
+        if (
+            d.boundWidth / rel > d.boundHeight &&
+            d.boundHeight * rel <= d.boundWidth
+        ) {
+            bound.width = Math.round(d.boundHeight * rel);
+            bound.height = d.boundHeight;
+        } else {
+            bound.width = d.boundWidth;
+            bound.height = Math.round(d.boundWidth / rel);
+        }
+    }
+    return bound;
+}

+ 26 - 0
src/views/analysisReport/studentPage/downloadPdf/components/js/zoom.min.js

@@ -0,0 +1,26 @@
+/* turn.js 4.1.0 | Copyright (c) 2012 Emmanuel Garcia | turnjs.com | turnjs.com/license.txt */
+
+(function(f){function r(a,b){return a[0]==b[0]?!1:a.attr("page")?!0:a.parent()[0]?r(a.parent(),b):!1}function u(a){function b(a){this.name="TurnJsError";this.message=a}b.prototype=Error();b.prototype.constructor=b;return new b(a)}function t(a,b,d){return q&&d?" translate3d("+a+"px,"+b+"px, 0px) ":" translate("+a+"px, "+b+"px) "}function v(a,b){return q&&b?" scale3d("+a+", "+a+", 1) ":" scale("+a+") "}function h(a,b){return{x:a,y:b}}function m(a,b){return function(){return a.apply(b,arguments)}}var q,
+w={max:2,flipbook:null,easeFunction:"ease-in-out",duration:500,when:{}},j={init:function(a){var b=this,d=this.data(),a=f.extend({},w,a);if(!a.flipbook||!a.flipbook.turn("is"))throw u("options.flipbook is required");q="WebKitCSSMatrix"in window||"MozPerspective"in document.body.style;if("function"!=typeof a.max){var e=a.max;a.max=function(){return e}}d.zoom={opts:a,axis:h(0,0),scrollPos:h(0,0),eventQueue:[],mouseupEvent:function(){return j._eMouseUp.apply(b,arguments)},eventTouchStart:m(j._eTouchStart,
+b),eventTouchMove:m(j._eTouchMove,b),eventTouchEnd:m(j._eTouchEnd,b),flipbookEvents:{zooming:m(j._eZoom,b),pressed:m(j._ePressed,b),released:m(j._eReleased,b),start:m(j._eStart,b),turning:m(j._eTurning,b),turned:m(j._eTurned,b),destroying:m(j._eDestroying,b)}};for(var c in a.when)Object.prototype.hasOwnProperty.call(a.when,c)&&this.bind("zoom."+c,a.when[c]);for(c in d.zoom.flipbookEvents)Object.prototype.hasOwnProperty.call(d.zoom.flipbookEvents,c)&&a.flipbook.bind(c,d.zoom.flipbookEvents[c]);this.css({position:"relative",
+overflow:"hidden"});f.isTouch?(a.flipbook.bind("touchstart",d.zoom.eventTouchStart).bind("touchmove",d.zoom.eventTouchMove).bind("touchend",d.zoom.eventTouchEnd),this.bind("touchstart",j._tap)):this.mousedown(j._mousedown).click(j._tap)},_tap:function(a){var b=f(this),d=b.data().zoom;!d.draggingCorner&&!d.dragging&&r(f(a.target),b)&&(j._addEvent.call(b,"tap",a),(a=j._eventSeq.call(b))&&b.trigger(a))},_addEvent:function(a,b){var d=this.data().zoom,e=(new Date).getTime();d.eventQueue.push({name:a,timestamp:e,
+event:b});10<d.eventQueue.length&&d.eventQueue.splice(0,1)},_eventSeq:function(){var a=this.data().zoom.eventQueue,b=a.length-1;if(0<b&&"tap"==a[b].name&&"tap"==a[b-1].name&&a[b].event.pageX==a[b-1].event.pageX&&a[b].event.pageY==a[b-1].event.pageY&&200>a[b].timestamp-a[b-1].timestamp&&50<a[b].timestamp-a[b-1].timestamp)return f.extend(a[b].event,{type:"zoom.doubleTap"});if("tap"==a[b].name)return f.extend(a[b].event,{type:"zoom.tap"})},_prepareZoom:function(){var a,b=0,d=this.data().zoom,e=1/this.zoom("value");
+a=d.opts.flipbook;var c=a.turn("direction"),j=a.data(),l=a.offset(),i=this.offset(),g={height:a.height()},k=a.turn("view");"double"==a.turn("display")&&a.data().opts.autoCenter?k[0]?k[1]?(g.width=a.width(),a=h(l.left-i.left,l.top-i.top)):(g.width=a.width()/2,b="ltr"==c?0:g.width,a=h("ltr"==c?l.left-i.left:l.left-i.left+g.width,l.top-i.top)):(g.width=a.width()/2,b="ltr"==c?g.width:0,a=h("ltr"==c?l.left-i.left+g.width:l.left-i.left,l.top-i.top)):(g.width=a.width(),a=h(l.left-i.left,l.top-i.top));d.zoomer||
+(d.zoomer=f("<div />",{"class":"zoomer",css:{overflow:"hidden",position:"absolute",zIndex:"1000000"}}).mousedown(function(){return!1}).appendTo(this));d.zoomer.css({top:a.y,left:a.x,width:g.width,height:g.height});c=k.join(",");if(c!=d.zoomerView){d.zoomerView=c;d.zoomer.find("*").remove();for(c=0;c<k.length;c++)if(k[c]){var i=j.pageObjs[k[c]].offset(),m=f(j.pageObjs[k[c]]);m.clone().transform("").css({width:m.width()*e,height:m.height()*e,position:"absolute",display:"",top:(i.top-l.top)*e,left:(i.left-
+l.left-b)*e}).appendTo(d.zoomer)}}return{pos:a,size:g}},value:function(){return this.data().zoom.opts.flipbook.turn("zoom")},zoomIn:function(a){var b=this,d=this.data().zoom,e=d.opts.flipbook,c=d.opts.max();e.offset();var s=this.offset();if(d.zoomIn)return this;e.turn("stop");var l=f.Event("zoom.change");this.trigger(l,[c]);if(l.isDefaultPrevented())return this;var i=j._prepareZoom.call(this),g=i.pos,k=h(i.size.width/2,i.size.height/2),l=f.cssPrefix(),m=f.cssTransitionEnd(),q=e.data().opts.autoCenter;
+d.scale=c;e.data().noCenter=!0;a="undefined"!=typeof a?"x"in a&&"y"in a?h(a.x-g.x,a.y-g.y):f.isTouch?h(a.originalEvent.touches[0].pageX-g.x-s.left,a.originalEvent.touches[0].pageY-g.y-s.top):h(a.pageX-g.x-s.left,a.pageY-g.y-s.top):h(k.x,k.y);if(0>a.x||0>a.y||a.x>i.width||a.y>i.height)a.x=k.x,a.y=k.y;var n=h((a.x-k.x)*c+k.x,(a.y-k.y)*c+k.y),a=h(i.size.width*c>this.width()?a.x-n.x:0,i.size.height*c>this.height()?a.y-n.y:0),n=h(Math.abs(i.size.width*c-this.width()),Math.abs(i.size.height*c-this.height())),
+i=h(Math.min(0,i.size.width*c-this.width()),Math.min(0,i.size.height*c-this.height())),p=h(k.x*c-k.x-g.x-a.x,k.y*c-k.y-g.y-a.y);p.y>n.y?a.y=p.y-n.y+a.y:p.y<i.y&&(a.y=p.y-i.y+a.y);p.x>n.x?a.x=p.x-n.x+a.x:p.x<i.x&&(a.x=p.x-i.x+a.x);p=h(k.x*c-k.x-g.x-a.x,k.y*c-k.y-g.y-a.y);g={};g[l+"transition"]=l+"transform "+d.opts.easeFunction+" "+d.opts.duration+"ms";var r=function(){b.trigger("zoom.zoomIn");d.zoomIn=!0;d.flipPosition=h(e.css("left"),e.css("top"));e.turn("zoom",c).css({position:"absolute",margin:"",
+top:0,left:0});var a=e.offset();d.axis=h(a.left-s.left,a.top-s.top);if(q&&"double"==e.turn("display")&&("ltr"==e.turn("direction")&&!e.turn("view")[0]||"rtl"==e.turn("direction")&&!e.turn("view")[1]))d.axis.x+=e.width()/2;b.zoom("scroll",p);b.bind(f.mouseEvents.down,j._eMouseDown);b.bind(f.mouseEvents.move,j._eMouseMove);f(document).bind(f.mouseEvents.up,d.mouseupEvent);b.bind("mousewheel",j._eMouseWheel);setTimeout(function(){d.zoomer.hide();d.zoomer.remove();d.zoomer=null;d.zoomerView=null},50)};
+d.zoomer.css(g).show();m?d.zoomer.bind(m,function(){f(this).unbind(m);r()}):setTimeout(r,d.opts.duration);d.zoomer.transform(t(a.x,a.y,!0)+v(c,!0));return this},zoomOut:function(a){var b,d=this,e=this.data().zoom,c=e.opts.flipbook,m=1/e.scale,l=f.cssPrefix(),i=f.cssTransitionEnd();b=this.offset();a="undefined"!=typeof a?a:e.opts.duration;if(e.zoomIn){var g=f.Event("zoom.change");this.trigger(g,[1]);if(g.isDefaultPrevented())return this;e.zoomIn=!1;e.scale=1;c.data().noCenter=!1;d.unbind(f.mouseEvents.down,
+j._eMouseDown);d.unbind(f.mouseEvents.move,j._eMouseMove);f(document).unbind(f.mouseEvents.up,e.mouseupEvent);d.unbind("mousewheel",j._eMouseWheel);g={};g[l+"transition"]=l+"transform "+e.opts.easeFunction+" "+a+"ms";c.css(g);var k=f("<div />",{css:{position:"relative",top:e.flipPosition.y,left:e.flipPosition.x,width:c.width()*m,height:c.height()*m,background:"blue"}}).appendTo(c.parent()),g=h(k.offset().left-b.left,k.offset().top-b.top);k.remove();var q=c.data().opts.autoCenter;q&&"double"==c.turn("display")&&
+(c.turn("view")[0]?c.turn("view")[1]||(g.x="ltr"==c.turn("direction")?g.x+k.width()/4:g.x-k.width()/4):g.x="ltr"==c.turn("direction")?g.x-k.width()/4:g.x+k.width()/4);var r=f.findPos(c[0]);b=h(-c.width()/2-r.left+k.width()/2+g.x+b.left,-c.height()/2-r.top+k.height()/2+g.y+b.top);var n=function(){c[0].style.removeProperty?(c[0].style.removeProperty(l+"transition"),c.transform(c.turn("options").acceleration?t(0,0,!0):"").turn("zoom",1),c[0].style.removeProperty("margin"),c.css({position:"relative",
+top:e.flipPosition.y,left:e.flipPosition.x})):c.transform("none").turn("zoom",1).css({margin:"",top:e.flipPosition.y,left:e.flipPosition.x,position:"relative"});q&&c.turn("center");d.trigger("zoom.zoomOut")};0===a?n():(i?c.bind(i,function(){f(this).unbind(i);n()}):setTimeout(n,a),c.transform(t(b.x,b.y,!0)+v(m,!0)));return this}},flipbookWidth:function(){var a=this.data().zoom.opts.flipbook,b=a.turn("view");return"double"==a.turn("display")&&(!b[0]||!b[1])?a.width()/2:a.width()},scroll:function(a,
+b,d){var e=this.data().zoom,c=e.opts.flipbook,j=this.zoom("flipbookWidth"),l=f.cssPrefix();if(q){var i={};i[l+"transition"]=d?l+"transform 200ms":"none";c.css(i);c.transform(t(-e.axis.x-a.x,-e.axis.y-a.y,!0))}else c.css({top:-e.axis.y-a.y,left:-e.axis.x-a.x});if(!b){var g,b=h(Math.min(0,(j-this.width())/2),Math.min(0,(c.height()-this.height())/2)),c=h(j>this.width()?j-this.width():(j-this.width())/2,c.height()>this.height()?c.height()-this.height():(c.height()-this.height())/2);a.y<b.y?(a.y=b.y,g=
+!0):a.y>c.y&&(a.y=c.y,g=!0);a.x<b.x?(a.x=b.x,g=!0):a.x>c.x&&(a.x=c.x,g=!0);g&&this.zoom("scroll",a,!0,!0)}e.scrollPos=h(a.x,a.y)},resize:function(){var a=this.data().zoom,b=a.opts.flipbook;if(1<this.zoom("value")){var d=b.offset(),e=this.offset();a.axis=h(d.left-e.left+(a.axis.x+a.scrollPos.x),d.top-e.top+(a.axis.y+a.scrollPos.y));"double"==b.turn("display")&&("ltr"==b.turn("direction")&&!b.turn("view")[0])&&(a.axis.x+=b.width()/2);this.zoom("scroll",a.scrollPos)}},_eZoom:function(){for(var a=this.data().zoom,
+b=a.opts.flipbook,d=b.turn("view"),e=0;e<d.length;e++)d[e]&&this.trigger("zoom.resize",[a.scale,d[e],b.data().pageObjs[d[e]]])},_eStart:function(a){1!=this.zoom("value")&&a.preventDefault()},_eTurning:function(a,b,d){var e=this,a=this.zoom("value"),c=this.data().zoom,b=c.opts.flipbook;c.page=b.turn("page");if(1!=a){for(c=0;c<d.length;c++)d[c]&&this.trigger("zoom.resize",[a,d[c],b.data().pageObjs[d[c]]]);setTimeout(function(){e.zoom("resize")},0)}},_eTurned:function(a,b){if(1!=this.zoom("value")){var d=
+this.data().zoom,e=d.opts.flipbook;b>d.page?this.zoom("scroll",h(0,d.scrollPos.y),!1,!0):b<d.page&&this.zoom("scroll",h(e.width(),d.scrollPos.y),!1,!0)}},_ePressed:function(){f(this).data().zoom.draggingCorner=!0},_eReleased:function(){var a=f(this).data().zoom;setTimeout(function(){a.draggingCorner=!1},1)},_eMouseDown:function(a){f(this).data().zoom.draggingCur=f.isTouch?h(a.originalEvent.touches[0].pageX,a.originalEvent.touches[0].pageY):h(a.pageX,a.pageY);return!1},_eMouseMove:function(a){var b=
+f(this).data().zoom;if(b.draggingCur){b.dragging=!0;var a=f.isTouch?h(a.originalEvent.touches[0].pageX,a.originalEvent.touches[0].pageY):h(a.pageX,a.pageY),d=h(a.x-b.draggingCur.x,a.y-b.draggingCur.y);f(this).zoom("scroll",h(b.scrollPos.x-d.x,b.scrollPos.y-d.y),!0);b.draggingCur=a;return!1}},_eMouseUp:function(){var a=f(this).data().zoom;a.dragging&&f(this).zoom("scroll",a.scrollPos);a.draggingCur=null;setTimeout(function(){a.dragging=!1},1)},_eMouseWheel:function(a,b,d,e){a=f(this).data().zoom;d=
+h(a.scrollPos.x+10*d,a.scrollPos.y-10*e);f(this).zoom("scroll",d,!1,!0)},_eTouchStart:function(a){var b=f(this).data().zoom,a=h(a.originalEvent.touches[0].pageX,a.originalEvent.touches[0].pageY);b.touch={};b.touch.initial=a;b.touch.last=a;b.touch.timestamp=(new Date).getTime();b.touch.speed=h(0,0)},_eTouchMove:function(a){var b=f(this).data().zoom,d=f(this).zoom("value"),e=b.opts.flipbook,c=(new Date).getTime(),a=h(a.originalEvent.touches[0].pageX,a.originalEvent.touches[0].pageY);b.touch&&(1==d&&
+!e.data().mouseAction)&&(b.touch.motion=h(a.x-b.touch.last.x,a.y-b.touch.last.y),b.touch.speed.x=0===b.touch.speed.x?b.touch.motion.x/(c-b.touch.timestamp):(b.touch.speed.x+b.touch.motion.x/(c-b.touch.timestamp))/2,b.touch.last=a,b.touch.timestamp=c)},_eTouchEnd:function(){var a=f(this).data().zoom;if(a.touch&&1==f(this).zoom("value")){var b=Math.abs(a.touch.initial.y-a.touch.last.y);50>b&&(-1>a.touch.speed.x||-100>a.touch.last.x-a.touch.initial.x)?this.trigger("zoom.swipeLeft"):50>b&&(1<a.touch.speed.x||
+100<a.touch.last.x-a.touch.initial.x)&&this.trigger("zoom.swipeRight")}},_eDestroying:function(){var a=this,b=this.data().zoom,d=b.opts.flipbook;this.zoom("zoomOut",0);f.each("tap doubleTap resize zoomIn zoomOut swipeLeft swipeRight".split(" "),function(b,d){a.unbind("zoom."+d)});for(var e in b.flipbookEvents)Object.prototype.hasOwnProperty.call(b.flipbookEvents,e)&&d.unbind(e,b.flipbookEvents[e]);d.unbind("touchstart",b.eventTouchStart).unbind("touchmove",b.eventTouchMove).unbind("touchend",b.eventTouchEnd);
+this.unbind("touchstart",j._tap).unbind("click",j._tap);b=null;this.data().zoom=null}};f.extend(f.fn,{zoom:function(){var a=arguments;if(!a[0]||"object"==typeof a[0])return j.init.apply(f(this[0]),a);if(j[a[0]])return j[a[0]].apply(f(this[0]),Array.prototype.slice.call(a,1));throw u(a[0]+" is not a method");}})})(jQuery);

+ 1665 - 0
src/views/analysisReport/studentPage/downloadPdf/studentReport.vue

@@ -0,0 +1,1665 @@
+<template>
+    <!-- 学生报告 -->
+    <!-- v-loading="loading" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.5)" :element-loading-text="loadingText" -->
+    <BookFlip :book-pages="bookPageImages" bookBg="student" :requestLoading="loading" :openLoading="openLoading" :coverTitle="`${stuClasName}${studentName}分析报告册`" subtitle="供参考学生使用" ref="bookFlipBox" @OpenBookImages="OpenBookImages" @PdfLoadEnd="PdfLoadEnd">
+        <!-- 自定义每页内容 -->
+        <template #default="slotProps">
+            <template v-if="slotProps.type == 'web_mode'">
+                <div class="area_page" style="position: absolute;top: -9999999px;z-index: -10;height: auto;min-height: 1285px;">
+                    <!-- 用于获取div的高度 默认隐藏不显示 -->
+                    <div class="area_title" ref="areaReportTitle">
+                        <p>{{ reportTitle }}</p>
+                        <p>{{ stuClasName }}{{ studentName }}分析报告</p>
+                    </div>
+                    <div class="area_module" ref="standardScoreChartDes">
+                        <div class="area_module_describe" v-if="multiSubjectData.maxSubject || multiSubjectData.minSubject" style="margin-top: 0;">
+                            说明:从标准分情况来看,这次考试<template v-if="multiSubjectData.maxSubject"><span style="color: #3ba272">{{ multiSubjectData.maxSubject }}</span>表现突出,请继续保持</template><template v-if="multiSubjectData.minSubject">;<span style="color: #ee6666">{{ multiSubjectData.minSubject }}</span>标准分明显低于其他学科,可能会对总体排名造成影响,可结合错题梳理核心知识点,精准定位薄弱环节,制定针对性的提升计划,以实现各科均衡发展,进一步巩固整体成绩</template>。
+                        </div>
+                    </div>
+                    <div class="area_module" ref="multiSuggestionModule">
+                        <div class="area_module_title">总结建议</div>
+                        <div class="area_module_describe" style="margin-top: 0;" v-html="suggestionHtml"></div>
+                        <div class="pring_jg"></div>
+                    </div>
+                    <template v-for="(subject,subKey) in multiSubjectData.singleSubject">
+                        <div class="area_module" :ref="`singleSubjectSuggestion_${subKey}`" v-if="singleSubjectData?.[subKey]?.suggestionHtml">
+                            <div class="area_module_title">总结建议</div>
+                            <div class="area_module_describe" style="margin-top: 0;" v-html="singleSubjectData?.[subKey]?.suggestionHtml"></div>
+                            <div class="pring_jg"></div>
+                        </div>
+                    </template>
+                    <!-- 知识点分层 -->
+                    <template v-for="know in knowLedgeLayeringData">
+                        <div class="area_module" :ref="`knowLedgeLayering_${know.subjectCode}`">
+                            <div class="area_module_title">{{know.groupName}}分层分析</div>
+                            <div class="area_module_table">
+                                <ul class="student_know_paper">
+                                    <li>
+                                        <div class="li_left">
+                                            <span>满分区</span>
+                                            <span>(得满分的知识点)</span>
+                                        </div>
+                                        <div class="li_know">{{ know.knowLedgeLayering.fullScore }}{{ know.knowLedgeLayering.fullScore ? '。':'' }}</div>
+                                    </li>
+                                    <li>
+                                        <div class="li_left">
+                                            <span>突破升级区</span>
+                                            <span>(得分率高于同层次,但低于上一层次平均水平的知识点)</span>
+                                        </div>
+                                        <div class="li_know">{{ know.knowLedgeLayering.breakThrough }}{{ know.knowLedgeLayering.breakThrough ? '。':'' }}</div>
+                                    </li>
+                                    <li>
+                                        <div class="li_left">
+                                            <span>就近发展区</span>
+                                            <span>(得分率低于同层次平均水平的知识点)</span>
+                                        </div>
+                                        <div class="li_know">{{ know.knowLedgeLayering.develop }}{{ know.knowLedgeLayering.develop ? '。':'' }}</div>
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="pring_jg"></div>
+                        </div>
+                    </template>
+                </div>
+                <div class="area_page web_area_page" v-for="page in pageCount" :key="`${pageKey}_${page}`">
+                    <template v-if="page == 1">
+                        <div class="area_title">
+                            <p>{{ reportTitle }}</p>
+                            <p>{{ stuClasName }}{{ studentName }}分析报告</p>
+                        </div>
+                        <div class="area_header bg_purple">
+                            <img :src="headerLeftIcon" class="header_icon_left" />
+                            总分成绩分析
+                            <img :src="headerRightIcon" class="header_icon_right" />
+                        </div>
+                        <div class="pring_jg"></div>
+                    </template>
+                    <template v-for="(tableData,index) in multiSubjectData.tableList">
+                        <template v-for="(itemTable,itemIndex) in tableData">
+                            <div class="area_module" v-if="multiSubjectData.tablePagesNum[index][itemIndex] == page && itemTable.length > 0">
+                                <div class="area_module_title">成绩单</div>
+                                <div class="area_module_table">
+                                    <el-table border :data="itemTable" stripe align="center">
+                                        <el-table-column v-for="header in (multiSubjectData?.staticHeaderData ?? [])" align="center" :label="header.name" min-width="100" show-overflow-tooltip>
+                                            <template slot-scope="scope">
+                                                {{ scope.row?.[header.prop] || '-' }}
+                                            </template>
+                                        </el-table-column> 
+                                        <el-table-column v-for="header in (multiSubjectData?.headerList?.[index]?.[itemIndex] ?? [])" align="center" :label="header.name" min-width="100" show-overflow-tooltip>
+                                            <template slot-scope="scope">
+                                                <template v-if="header.prop == 'score'">
+                                                    <!-- * 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
+                                                    * 3-得分显示对错,小题分显示分数,4-得分显示对错,小题分显示对错
+                                                    * 5-得分显示等级,小题分显示分数,6-得分显示等级,小题分显示对错 -->
+                                                    <template v-if="multiSubjectData.studentOpenness == 3 || multiSubjectData.studentOpenness == 4">
+                                                        <template v-if="!isNaN(scope.row?.[header.prop])">
+                                                            <img class="right_or_wrong_icon" v-if="scope.row.fullScore == scope.row?.[header.prop]" src="@/assets/report/score_yes_icon.webp" />
+                                                            <img class="right_or_wrong_icon" v-else-if="scope.row?.[header.prop] == 0" src="@/assets/report/score_no_icon.webp" />
+                                                            <img class="right_or_wrong_icon" v-else src="@/assets/report/score_dimidiate_icon.webp" />
+                                                        </template>
+                                                        <template v-else>{{ scope.row?.[header.prop] ?? '-' }}</template>
+                                                    </template>
+                                                    <template v-else>{{ scope.row?.[header.prop] ?? '-' }}</template>
+                                                </template>
+                                                <template v-else-if="header.prop == 'groupClassRanks' || header.prop == 'groupClassMaxScoreList' || header.prop == 'groupClassAvgScoreList'">{{ GetGroupClassValue(scope.row?.[header.prop],header.prop,header.code)}}</template>
+                                                <template v-else>{{ scope.row?.[header.prop] ?? '-' }}</template>
+                                                </template>
+                                        </el-table-column>
+                                    </el-table>
+                                </div>
+                            </div>
+                            <template v-if="multiSubjectData.tablePagesNum[index][itemIndex] == page && itemTable.length > 0">
+                                <div class="pring_jg" style="height: 29px;border-bottom: 1px solid #F3F3F3;box-sizing: border-box;"></div>
+                                <div class="pring_jg"></div>
+                            </template>
+                        </template>
+                    </template>
+                    <div class="area_module" v-if="multiSubjectData.chartPagesNum==page && ((multiSubjectData.standardScoreAnalysisStatus === 0 && schoolType == 2) || schoolType == 1) && multiSubjectData.datay.length>0">
+                        <div class="area_module_title">标准分分析图</div>
+                        <div class="area_module_chart">
+                            <DifferenceChart v-if="multiSubjectData.datay.length" :datax="multiSubjectData.datax" unit="" :datay="multiSubjectData.datay" :isClick="false" :rate="0" :gridLeft="0" :gridRight="0" :gridTop="20" :fontSize="14" :fontColor="'#333333'" :showDataZoom="false" style="height: 270px !important;min-height: 270px !important;"></DifferenceChart>
+                        </div>
+                        <div class="area_module_describe" v-if="multiSubjectData.maxSubject || multiSubjectData.minSubject">
+                            说明:从标准分情况来看,这次考试<template v-if="multiSubjectData.maxSubject"><span style="color: #3ba272">{{ multiSubjectData.maxSubject }}</span>表现突出,请继续保持</template><template v-if="multiSubjectData.minSubject">;<span style="color: #ee6666">{{ multiSubjectData.minSubject }}</span>标准分明显低于其他学科,可能会对总体排名造成影响,可结合错题梳理核心知识点,精准定位薄弱环节,制定针对性的提升计划,以实现各科均衡发展,进一步巩固整体成绩</template>。
+                        </div>
+                        <div class="pring_jg"></div>
+                    </div>
+                    <div class="area_module" v-if="historyExamData.pageNum==page && historyExamData.chartData.length > 0 && multiSubjectData.showHistory==1">
+                        <div class="area_module_title">历次标准分追踪分析图</div>
+                        <div class="area_module_chart">
+                            <LineChart v-if="historyExamData.datay.length>0" :datax="historyExamData.datax" :datay="historyExamData.datay" :title="historyExamData.title"
+                                :extraText="false" :showBackground="false" :isShowLabel="true" labelColor="#333333" :legendList="historyExamData.legendList" :tooltipData="historyExamData.tooltipData" :gridLeft="0" :gridRight="0" :gridTop="25" :fontSize="14" :fontColor="'#333333'" :showCheckBox="false" reportHeight="270px" style="min-height:270px!important;">
+                            </LineChart>
+                        </div>
+                        <div class="pring_jg"></div>
+                    </div>
+                    <div class="area_module" v-if="suggestionHtml && multiSuggestionPageNum == page">
+                        <div class="area_module_title">总结建议</div>
+                        <div class="area_module_describe" style="margin-top: 0;" v-html="suggestionHtml"></div>
+                        <div class="pring_jg"></div>
+                    </div>
+                    <!-- 单科 -->
+                    <template v-for="(subject,subKey) in multiSubjectData.singleSubject">
+                        <template v-if="singleSubjectData?.[subKey]?.titlePageNum == page">
+                            <div class="area_header bg_purple">
+                                <img :src="headerLeftIcon" class="header_icon_left" />
+                                {{subject.subjectName}}成绩分析
+                                <img :src="headerRightIcon" class="header_icon_right" />
+                            </div>
+                            <div class="pring_jg"></div>
+                        </template>
+                        <!-- 成绩单 -->
+                        <template v-for="(tableData,index) in (singleSubjectData?.[subKey]?.scrolTableList || [])">
+                            <template v-for="(itemTable,itemIndex) in tableData">
+                                <div class="area_module" v-if="singleSubjectData?.[subKey]?.scrolTablePagesNum?.[index]?.[itemIndex] == page && itemTable.length > 0">
+                                    <div class="area_module_title">成绩单</div>
+                                    <div class="area_module_table">
+                                        <el-table border :data="itemTable" stripe align="center">
+                                            <el-table-column v-for="header in (singleSubjectData?.[subKey]?.scrolHeaderList?.[index]?.[itemIndex] ?? [])" align="center" :label="header.name" min-width="100" show-overflow-tooltip>
+                                                <template slot-scope="scope">
+                                                    <template v-if="header.prop=='score'">
+                                                        <!-- * 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
+                                                        * 3-得分显示对错,小题分显示分数,4-得分显示对错,小题分显示对错
+                                                        * 5-得分显示等级,小题分显示分数,6-得分显示等级,小题分显示对错 -->
+                                                        <template v-if="(scope.row.studentOpenness == 3 || scope.row.studentOpenness == 4) && scope.row?.score">
+                                                            <template v-if="!isNaN(scope?.row?.score)">
+                                                                <img class="right_or_wrong_icon" v-if="scope.row.fullScore == scope.row.score" src="@/assets/report/score_yes_icon.webp" />
+                                                                <img class="right_or_wrong_icon" v-else-if="scope.row.score === 0" src="@/assets/report/score_no_icon.webp" />
+                                                                <img class="right_or_wrong_icon" v-else src="@/assets/report/score_dimidiate_icon.webp" />
+                                                            </template>
+                                                            <template v-else>{{ scope.row.score }}</template>
+                                                        </template>
+                                                        <template v-else>{{ scope?.row?.[header.prop] ?? '-' }}</template>
+                                                    </template>
+                                                    <template v-else-if="header.prop == 'groupClassRanks' || header.prop == 'groupClassMaxScoreList' || header.prop == 'groupClassAvgScoreList'">{{ GetGroupClassValue(scope.row?.[header.prop],header.prop,header.code) }}</template>
+                                                    <template v-else>{{ scope?.row?.[header.prop] ?? '-' }}</template>
+                                                </template>
+                                            </el-table-column>
+                                        </el-table>
+                                    </div>
+                                </div>
+                                <template v-if="singleSubjectData?.[subKey]?.scrolTablePagesNum?.[index]?.[itemIndex] == page && itemTable.length > 0">
+                                    <div class="pring_jg" style="height: 29px;border-bottom: 1px solid #F3F3F3;box-sizing: border-box;"></div>
+                                    <div class="pring_jg"></div>
+                                </template>
+                            </template>
+                        </template>
+                        <!-- 小题分 大题 知识点  能力要素  自定义分组 -->
+                        <template v-for="group in (singleSubjectData?.[subKey]?.groupQuestionData || [])">
+                            <div class="area_module" v-if="group.chartPagesNum == page && group.datay.length > 0 && group.type!='smallQuestionData'">
+                                <div class="area_module_title">{{group.groupName}}分析图</div>
+                                <div class="area_module_chart" v-if="group.datay.length > 0">
+                                    <BarChart v-if="group.type=='bigQuestionData'" :datax="group.datax" :datay="group.datay" unit="" typeName="" :showNuitY="false" :unit="'%'" :showTooltip="false" :isShowMarkLine="false" :gridLeft="0" :gridRight="0" :gridTop="20" :fontSize="14" :fontColor="'#333333'" :showDataZoom="false" style="height: 270px;"></BarChart>
+                                    <RadarCharts v-else :showLegend="false" :reportHeight="`300px`" :showDataLabel="true" :showTooltip="false" :data="group.radarChartData" :unit="'%'" :legendList="[]" legendLeft="center" :showCheckBox="false" :openShowAllLegend="false" :showRadiusAxis="false" :fontSize="14" :fontColor="'#333333'" :style="{height: 'auto',minHeight:'100% !important'}"></RadarCharts>
+                                </div>
+                                <div class="pring_jg"></div>
+                            </div>
+                            <template v-for="(tableData,index) in group.tableList">
+                                <template v-for="(itemTable,itemIndex) in tableData">
+                                    <div class="area_module" v-if="group.tablePagesNum[index][itemIndex] == page && itemTable.length > 0">
+                                        <div class="area_module_title">{{group.groupName}}分析表</div>
+                                        <div class="area_module_table">
+                                            <el-table border :data="itemTable" stripe align="center">
+                                                <el-table-column v-for="header in (group?.staticHeader ?? [])" align="center" :label="header.name" :min-width="header.prop=='smallQuestionNames'?120:90" show-overflow-tooltip>
+                                                    <template slot-scope="scope">
+                                                        {{ scope.row[header.prop] || '-' }}
+                                                    </template>
+                                                </el-table-column>    
+                                                <el-table-column v-for="header in (group?.headerList?.[index]?.[itemIndex] ?? [])" align="center" :label="header.name" :min-width="header.prop=='smallQuestionNames'?120:90" show-overflow-tooltip>
+                                                    <template slot-scope="scope">
+                                                        <template v-if="header.prop == 'score'">
+                                                            <!-- * 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
+                                                            * 3-得分显示对错,小题分显示分数,4-得分显示对错,小题分显示对错
+                                                            * 5-得分显示等级,小题分显示分数,6-得分显示等级,小题分显示对错 -->
+                                                            <template v-if="group.studentOpenness == 2 || group.studentOpenness == 4 || group.studentOpenness == 6">
+                                                                <template v-if="!isNaN(scope?.row?.score)">
+                                                                    <img class="right_or_wrong_icon" v-if="scope.row.fullScore == scope.row.score" src="@/assets/report/score_yes_icon.webp" />
+                                                                    <img class="right_or_wrong_icon" v-else-if="scope.row.score == 0" src="@/assets/report/score_no_icon.webp" />
+                                                                    <img class="right_or_wrong_icon" v-else src="@/assets/report/score_dimidiate_icon.webp" />
+                                                                </template>
+                                                                <template v-else>{{ scope?.row?.score ?? '-' }}</template>
+                                                            </template>
+                                                            <template v-else>{{ scope.row.score || '-' }}</template>
+                                                        </template>
+                                                        <!-- 包含小题 -->
+                                                        <template v-else-if="header.prop == 'smallQuestionNames' && scope?.row?.[header.prop] && Object.prototype.toString.call(scope.row[header.prop]) == '[object Array]'">
+                                                            {{ scope.row[header.prop].join('、') }}
+                                                        </template>
+                                                        <template v-else-if="header.prop=='scoreRateStr'">
+                                                            <template v-if="scope?.row?.scoreRate && !isNaN(scope?.row?.scoreRate)">
+                                                                <span style="color:#EE6666;" v-if="Number(scope?.row?.scoreRate) < 60">{{ scope?.row?.[header.prop] }}</span>
+                                                                <span style="color:#FAC858;" v-else-if="Number(scope?.row?.scoreRate) >= 60 && Number(scope?.row?.scoreRate) < 80">{{ scope?.row?.[header.prop] }}</span>
+                                                                <span style="color:#3BA272;" v-else-if="Number(scope?.row?.scoreRate) >= 80 && Number(scope?.row?.scoreRate) <= 100">{{ scope?.row?.[header.prop] }}</span>
+                                                                <span v-else>{{ scope?.row?.[header.prop] ?? '-'}}</span>
+                                                            </template>
+                                                            <template v-else>{{ scope?.row?.[header.prop] ?? '-'}}</template>
+                                                        </template>
+                                                        <template v-else-if="header.prop == 'difficultyName'">
+                                                            <span :class="GetDifficultyClass(scope.row[header.prop])"></span>
+                                                            <span>{{ GetDifficultyName(scope.row[header.prop]) }}</span>
+                                                        </template>
+                                                        <template v-else-if="header.prop == 'classGroupAvgScoreMap' || header.prop == 'classGroupScoreRateMap'">{{ scope.row?.[header.prop]?.[header.code] || '-' }}</template>
+                                                        <template v-else>{{ scope.row[header.prop] || '-' }}</template>
+                                                    </template>
+                                                </el-table-column>
+                                            </el-table>
+                                        </div>
+                                    </div>
+                                    <template v-if="group.tablePagesNum[index][itemIndex] == page && itemTable.length > 0">
+                                        <div class="pring_jg" style="height: 29px;border-bottom: 1px solid #F3F3F3;box-sizing: border-box;"></div>
+                                        <div class="pring_jg"></div>
+                                    </template>
+                                </template>
+                            </template>
+                            <div class="area_module" v-if="group.type=='knowledgePointQuestionData' && group?.knowLedgeLayering && page == knowLedgeLayeringPageNum[subKey]">
+                                <div class="area_module_title">{{group.groupName}}分层分析</div>
+                                <div class="area_module_table">
+                                    <ul class="student_know_paper">
+                                        <li>
+                                            <div class="li_left">
+                                                <span>满分区</span>
+                                                <span>(得满分的知识点)</span>
+                                            </div>
+                                            <div class="li_know">{{ group.knowLedgeLayering.fullScore }}{{ group.knowLedgeLayering.fullScore ? '。':'' }}</div>
+                                        </li>
+                                        <li>
+                                            <div class="li_left">
+                                                <span>突破升级区</span>
+                                                <span>(得分率高于同层次,但低于上一层次平均水平的知识点)</span>
+                                            </div>
+                                            <div class="li_know">{{ group.knowLedgeLayering.breakThrough }}{{ group.knowLedgeLayering.breakThrough ? '。':'' }}</div>
+                                        </li>
+                                        <li>
+                                            <div class="li_left">
+                                                <span>就近发展区</span>
+                                                <span>(得分率低于同层次平均水平的知识点)</span>
+                                            </div>
+                                            <div class="li_know">{{ group.knowLedgeLayering.develop }}{{ group.knowLedgeLayering.develop ? '。':'' }}</div>
+                                        </li>
+                                    </ul>
+                                </div>
+                                <div class="pring_jg"></div>
+                            </div>
+                        </template>
+                        <!-- 历次 -->
+                        <div class="area_module" v-if="singleSubjectData?.[subKey]?.historyExamData?.pageNum==page && singleSubjectData[subKey].historyExamData.chartData.length > 0 && singleSubjectData[subKey]?.showHistory==1">
+                            <div class="area_module_title">历次标准分追踪分析图</div>
+                            <div class="area_module_chart">
+                                <LineChart v-if="singleSubjectData?.[subKey]?.historyExamData?.datay.length>0" :datax="singleSubjectData[subKey].historyExamData.datax" :datay="singleSubjectData[subKey].historyExamData.datay" :title="singleSubjectData[subKey].historyExamData.title"
+                                    :extraText="false" :showBackground="false" :isShowLabel="true" labelColor="#333333" :legendList="singleSubjectData[subKey].historyExamData.legendList" :tooltipData="singleSubjectData[subKey].historyExamData.tooltipData" :gridLeft="0" :gridRight="0" :gridTop="25" :fontSize="14" :fontColor="'#333333'" :showCheckBox="false" reportHeight="270px" style="min-height:270px!important;">
+                                </LineChart>
+                            </div>
+                            <div class="pring_jg"></div>
+                        </div>
+                        <div class="area_module" v-if="singleSubjectData?.[subKey]?.suggestionHtml && singleSubjectData?.[subKey]?.suggestionPageNum == page">
+                            <div class="area_module_title">总结建议</div>
+                            <div class="area_module_describe" style="margin-top: 0;" v-html="singleSubjectData?.[subKey]?.suggestionHtml"></div>
+                            <div class="pring_jg"></div>
+                        </div>
+                        <!-- 答题卡 -->
+                        <template v-for="(paperItem,paperIndex) in (singleSubjectData?.[subKey]?.paperImageList || [])">
+                            <div class="area_module area_module_img" v-if="singleSubjectData?.[subKey]?.paperImagePageNum?.[paperIndex] == page">
+                                <PaperImage v-if="paperItem.picUrl" :paperImgUrl="paperItem.picUrl" :usedCardType="singleSubjectData?.[subKey]?.usedCardType" :drawData="paperItem.questionVOS || []" :isDrag="false" :isWheel="false" :isShowContextMenu="false" rotateDeg="-90"></PaperImage>
+                            </div>
+                        </template>
+                    </template>
+                    <div class="area_page_number">第 {{ page }} 页</div>
+                </div>
+            </template>
+            <template v-else>
+                <!-- 生成翻书效果 -->
+                <div class="gradient"></div>
+                <img :src="slotProps.content" style="width: 100%;height: 100%;" />
+            </template>
+        </template>
+    </BookFlip>
+</template>
+
+<script>
+import BookFlip from './components/bookFlip.vue';
+import BarChart from "@/views/analysisReport/components/dCharts/barChart"; //单柱状图组件
+import RadarCharts from "@/views/analysisReport/components/dCharts/radarCharts";//G10-G1雷达图
+import DifferenceChart from "@/views/analysisReport/components/dCharts/differenceChart"; //率差图
+import LineChart from "@/views/analysisReport/components/dCharts/lineChart";//折线图
+import PaperImage from '@/components/PaperImage.vue';//答题卡
+import { mapGetters } from "vuex";
+import {getApiName} from '@/utils/common';
+// import { jsPDF } from "jspdf";
+import html2canvas from "html2canvas";
+export default {
+    components: { BookFlip, BarChart, RadarCharts, DifferenceChart, LineChart,PaperImage},
+    data() {
+        return {
+            pageKey:1,
+            headerLeftIcon: require("@/assets/report/header_left_student.webp"),
+            headerRightIcon: require("@/assets/report/header_right_student.webp"),
+            // 书页数据(长度为4,偶数页)
+            colors: this.$global.getScorePerformanceAnalysis(),//按顺序显示的20个颜色值
+            pageCount:15,//页数 第一页不处理
+            printPageHeight:1245,//A4纸高度 去掉边距 40
+            chartHeight:270,//echart 高度
+            moduleHeightData:[],//每个模块的高度
+            modulePageData:[],//每个模块对应的页码
+            stuClasName:'',
+            multiSubjectData: {
+                tablePagesNum:[],
+                tableList:[],
+                headerList:[],
+                staticHeaderData:[],
+                singleSubject:[],
+                standardScoreAnalysisStatus: 0,
+                studentOpenness: '', //学生信息
+                datax: [], //图数据
+                datay: [], //图数据
+                tooltipData: [],
+                maxSubject: '',
+                minSubject: '',
+                showHistory:0
+            },//总分成绩分析
+            historyExamData: {
+                pageNum:'',
+                chartData: [],
+                datax: [],
+                datay: [],
+                title: [],
+                legendList: [],
+                tooltipData:[]
+            }, //总分历次信息(联考)
+            suggestionHtml: null,//总结建议
+            multiSuggestionPageNum:'',//总分 总结建议
+            singleSubjectData:[],//单科数据
+            knowLedgeLayeringData:[],//知识点分层
+            knowLedgeLayeringPageNum:[],//知识点分层分页
+            bookPageImages:[],//生成的图片地址
+            // showReportLoading:false,
+            targetProgress:0,
+            timer:null,
+            loading:true,
+            loadingText:'拼命加载中(1%)……',
+            openLoading:false,
+        };
+    },
+    computed: {
+        reportTitle() {
+            return this?.$store?.state?.report?.examSelectItem?.examName ?? '';
+        },
+        reportParam() {
+            return {
+                examLevel: this.$store.state.report.examLevel, // 任务等级联考还是单校
+                // schoolId: this.$store.state.report.studentReportFilterObject.schoolId, // 学校id
+                // contrastExamIds: this.$store.state.report.lastExamSelectIds, //多次考试任务对比ID,不包含当前任务ID
+                // examId: this.$store.state.report.examId, // 当前考试ID
+                // subjectCode: this.$store.state.report.studentReportFilterObject.subjectCode, //科目code
+                // subjectGroupType: this.$store.state.report.filterObject.subjectGroupType, // 科目是否为组合
+                // isTotal: this.$store.state.report.filterObject.isTotal, //是否为总分科目 1为总分 0为非总分
+                // classIdCode: this.$store.state.report.studentReportFilterObject.classIdCode, // 班级idcode
+                // classGroupName: this.$store.state.report.studentReportFilterObject.classGroupName, // 班级名称
+                // registrationName: this.$store.state.report.studentReportFilterObject.registrationName, // 学籍名称
+                // studentName:this.$store.state.report.studentReportFilterObject.studentName, //学生名称
+                // registrationCode:this.$store.state.report.studentReportFilterObject.registrationCode //学生学籍号
+
+                examLevel: this.$store.state.report.filterObject.examLevel, //1-联考 2-单校
+                contrastExamIds: this.$store.state.report.filterObject.contrastExamIds, //多次考试任务对比ID,不包含当前任务ID
+                examId: this.$store.state.report.filterObject.examId, //考试id
+                subjectCode: this.$store.state.report.filterObject.subjectCode, //科目code
+                subjectGroupType: this.$store.state.report.filterObject.subjectGroupType, //是否为组合科目 1为组合科目 0为非组合科目
+                isTotal: this.$store.state.report.filterObject.isTotal //是否为总分科目 1为总分 0为非总分
+            }
+        },//分析报告公共参数变量
+        ...mapGetters(["userInfo"]),
+        studentName() {
+            return this.userInfo.userName;
+        },
+        schoolType() {
+            return sessionStorage.getItem('schoolType') //1:单校 2:联校
+        },
+    },
+    watch: {
+        // 监听 filterObject 对象变化(注意要开启深度监听)
+        async reportParam() {
+            // clearTimeout(this.timer); // 清除定时器,停止重复执行
+            // this.timer = null;
+            this.PageInit();//初始加载数据
+        },//监听筛选数据变化
+    },
+    async created() {
+        this.setHtmlFontSize();
+        // 监听窗口缩放,实时更新
+        window.addEventListener('resize', this.setHtmlFontSize);
+        // 可选:监听页面加载完成后再设置一次(避免初始渲染问题)
+        window.addEventListener('load', this.setHtmlFontSize);
+        this.PageInit();//页面初始加载数据
+    },
+    destroyed(){
+        // 根组件卸载时销毁监听
+        window.removeEventListener('resize', this.setHtmlFontSize);
+        document.documentElement.style.fontSize = ''; 
+    },
+    methods: {
+        //初始化设置
+        setHtmlFontSize(){
+            // 设计稿宽度(根据实际设计稿调整,例如 1920px)
+            const designWidth = 1920;
+            // 设计稿中 1rem 对应的 px 值(例如 16px)
+            const baseFontSize = 16;
+
+            // 获取当前浏览器窗口宽度
+            const windowWidth = document.documentElement.clientWidth || window.innerWidth;
+
+            // 计算当前窗口下的 html font-size(按设计稿比例缩放)
+            const htmlFontSize = (windowWidth / designWidth) * baseFontSize;
+            // 应用到 html 元素
+            document.documentElement.style.fontSize = `${htmlFontSize}px`;
+        },
+        async PageInit() {
+            // let currentProgress = 1;
+            // this.loadingText = `拼命加载中(1%)……`;
+            // this.timer = setInterval(() => {
+            //     currentProgress++;
+            //     this.loadingText = `拼命加载中(${currentProgress}%)……`;
+            // }, 500);
+            this.loading = true;
+            this.openLoading = false;
+            this.bookPageImages = [];
+            // this.targetProgress = 1;
+            // this.showReportLoading = true;
+            this.pageCount = 15;
+            this.$nextTick(()=>{
+                const titleHeight = this.$refs?.areaReportTitle?.offsetHeight || 0;
+                const reportTitleHeight = titleHeight + 82;
+                this.moduleHeightData = [reportTitleHeight];//每个模块的高度 初始标题高度
+            })
+            this.modulePageData = [1];
+            this.multiSuggestionPageNum = '';//总分 总结建议
+            this.singleSubjectData = [];//单科数据
+            this.knowLedgeLayeringData = [];//知识点分层
+            this.knowLedgeLayeringPageNum = [];//知识点分层分页
+            this.pageKey += 1;
+            //多科成绩总览 科目标准分分析
+            await this.QueryMultiSubjectData();
+            //学生端查询总分,多科历次信息(联考)
+            // await this.QueryHistoryExamData();
+            // 学生端查询总分,多科总结建议信息(联考)
+            await this.QuerySuggestionData();
+            let index = 0;
+            for (const element of this.multiSubjectData.singleSubject){
+                //单科每个模块标题分页
+                this.singleSubjectData.push({
+                    titlePageNum:this.SingleChartPage(82),
+                    scrolTablePagesNum:[],//成绩单
+                    scrolTableList:[],
+                    scrolHeaderList:[],
+                    groupQuestionData:[],//小题分组、大题分组、知识点、自定义分组
+                    historyExamData:{
+                        pageNum:'',
+                        chartData: [],
+                        datax: [],
+                        datay: [],
+                        title: [],
+                        legendList: [],
+                        tooltipData:[]
+                    },
+                    suggestionPageNum:'',//总结建议分页
+                    suggestionHtml: '',//总结建议
+                    paperImageList:[],//答题卡
+                    paperImagePageNum:[],//答题卡页码
+                    showHistory:0
+                })
+                await this.QueryOneSubjectData(element.subjectCode,index);
+                await this.QueryOneSubjectSmallQuestionData(element.subjectCode,index);
+                await this.QueryOneSubjectGroupQuestionData(element.subjectCode,index);
+                await this.QueryOneSubjectCustomGroupQuestion(element.subjectCode,index);
+                //学生端查询单科-历次查询(联考)
+                // await this.QueryOneSubjectHistoryExamData(element.subjectCode,index);
+                await this.QueryOneSubjectSuggestionData(element.subjectCode,index);
+                await this.FindStudentCard(element.subjectCode,index);
+                index++; // 每次循环后索引+1
+            }
+            this.pageCount = this.modulePageData.length > 0 ? Math.max(...this.modulePageData) : 1;
+            this.loading = false;
+            this.$emit('isPdfDataLoadEnd');//是否显示下载pdf按钮
+            console.log(this.pageCount,this.modulePageData,this.moduleHeightData,777777)
+            // this.$nextTick(()=>{
+            //     setTimeout(()=>{
+            //         // clearTimeout(this.timer); // 清除定时器,停止重复执行
+            //         // this.GetPageImages(currentProgress);
+            //         this.GetPageImages();
+            //     },2000)
+            // })
+        },
+        //下载预览图片
+        OpenBookImages(){
+            if(this.bookPageImages.length){
+                this.$refs.bookFlipBox.showFullScreen()
+            }else{
+                this.openLoading = true;
+                this.$nextTick(()=>{
+                    setTimeout(()=>{
+                        this.GetPageImages();
+                    },2000)
+                })
+            }
+        },
+        //获取图片
+        async GetPageImages(){
+            //获取所有的area_page 元素
+            const elements = document.querySelectorAll(".web_mode .web_area_page");
+            // const stepLens = elements?.length || 1;
+            // const remainingProgress = 100 - progress;//剩余进度
+            // const perStepProgress = Math.floor(remainingProgress / stepLens) || 1; // 每次增加的进度 向下取整
+            // const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
+            // const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
+            // const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
+            // let yPos = 0; //当前图像在pdf页面上的垂直位置的变量
+
+            let i = 1;
+            // let currentProgress = progress;
+            for (const element of elements) {
+                await html2canvas(element, {
+                    scale: 2, // 增加缩放比例
+                    useCORS: true, // 允许跨域
+                    logging: false, // 是否打印调试日志(开发时开启,生产关闭)
+                    letterRendering: true, // 文字抗锯齿 启用字母渲染
+                }).then(async (canvas) => {
+                    // const imgData = canvas.toDataURL("image/png");
+                    const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
+                    const imgUrl = URL.createObjectURL(blob);
+                    console.log(imgUrl,89999)
+                    this.bookPageImages.push(imgUrl);
+                    // currentProgress  = Math.min(Math.round((i / stepLens) * 100),100);
+                    // currentProgress  = Math.min(progress + i * perStepProgress,100);
+                    // this.targetProgress = currentProgress
+                    // this.loadingText = `拼命加载中(${currentProgress}%)……`;
+
+
+                    // const imgProps = pdf.getImageProperties(imgData); // 获取图像的属性,包括宽度和高度
+                    // const imgWidth = imgProps.width;
+                    // const imgHeight = imgProps.height;
+                    // const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight); //计算图片的缩放比例
+                    // const adjustWidth = imgWidth * ratio;
+                    // const adjustHeight = imgHeight * ratio;
+
+                    // // 在添加每个图像之前,检查当前页面的高度是否足够。如果不够,则添加新页面,并将 yPos 重置为 0
+                    // if (yPos + adjustHeight > pdfHeight) {
+                    //     pdf.addPage();
+                    //     yPos = 0;
+                    // }
+
+                    // // 将图像添加到pdf中
+                    // pdf.addImage(imgData, "PNG", 0, yPos, adjustWidth, adjustHeight);
+                    // yPos += adjustHeight;
+
+                    // // 如果添加图像后剩余空间不足一页,则添加新页面
+                    // if (yPos > pdfHeight) {
+                    //     pdf.addPage();
+                    //     yPos = 0;
+                    // }
+
+
+                    i++;
+                })
+            }
+            // if(currentProgress < 100){
+            //     this.loadingText = `拼命加载中(100%)……`;
+            // }
+            // 保存pdf文件
+            // pdf.save("联校报告册.pdf");
+            // this.showReportLoading = false;
+            setTimeout(()=>{
+                // this.loading = false;
+                this.openLoading = false;
+                this.$refs.bookFlipBox.showFullScreen()
+            },500)
+        },
+        //总分成绩分析
+        async QueryMultiSubjectData() {
+            await this.$api.reportStudent[getApiName()].queryMultiSubjectData({
+                ...this.reportParam,
+                subjectGroupType: 1, // 科目是否为组合
+                isTotal: 1, //是否为总分科目 1为总分 0为非总分
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const titleData = res?.data?.titleData || [];
+                    this.multiSubjectData.studentOpenness = res?.data?.studentOpenness ?? '';
+                    this.multiSubjectData.showHistory = res?.data?.showHistory || 0;
+                    const headerData = titleData.filter(item=>item.prop!='imgUrlList' && item.prop!='subjectName');
+                    const staticHeaderData = titleData.filter(item=>item.prop=='subjectName');
+                    const tableData = res?.data?.tableData || [];
+                    this.stuClasName = tableData?.find(item=>item.subjectCode==0)?.className || '';
+                    //表格分页
+                    const pageTableData = this.TableRowAndColumnPage(headerData,tableData,7,1);//8列一个表
+                    this.multiSubjectData.tablePagesNum = pageTableData.tablePagesNum;
+                    this.multiSubjectData.tableList = pageTableData.tableList;
+                    this.multiSubjectData.headerList = pageTableData.headerList;
+                    this.multiSubjectData.staticHeaderData = staticHeaderData;//静态表头
+
+                    //标准分分析图
+                    this.multiSubjectData.datax = []
+                    this.multiSubjectData.datay = []
+                    this.multiSubjectData.tooltipData = []
+                    this.multiSubjectData.standardScoreAnalysisStatus = res?.data?.standardScoreAnalysisStatus ?? 1;
+                    //总分
+                    const totalScore = tableData.filter(item => item.isTotal == 1)
+                    //组合
+                    const subjectGroup = tableData.filter(
+                        item => item.isTotal == 0 && item.subjectGroupType == 1
+                    )
+                    //单科
+                    const singleSubject = tableData.filter(
+                        item => item.isTotal == 0 && item.subjectGroupType == 0
+                    )
+                    this.multiSubjectData.singleSubject = singleSubject;//单科
+                    const chartData = [...totalScore, ...subjectGroup, ...singleSubject];
+                    let datax = [],datay = [];
+                    chartData.forEach(item => {
+                        this.multiSubjectData.datax.push(item?.subjectName ?? '-')
+                        this.multiSubjectData.datay.push(!item?.standardScore || item?.standardScore == '-' ? 0 : item?.standardScore)
+                        if (item.isTotal == 0 && item.subjectGroupType == 0) {
+                            datax.push(item?.subjectName ?? '')
+                            datay.push(item?.standardScore ?? 0)
+                        }
+                    })
+                    let maxSubject = [],minSubject = []
+                    datay.forEach((item, k) => {
+                        if (Number(item) > 0) {
+                            maxSubject.push(datax[k])
+                        }
+                        if (Number(item) < 0) {
+                            minSubject.push(datax[k])
+                        }
+                    })
+                    this.multiSubjectData.maxSubject = maxSubject.join('、')
+                    this.multiSubjectData.minSubject = minSubject.join('、')
+                    //标准分分析图分页
+                    this.$nextTick(()=>{
+                        if((this.multiSubjectData.standardScoreAnalysisStatus === 0 && this.schoolType == 2) || this.schoolType == 1){
+                            const desHeight = this.$refs?.standardScoreChartDes?.offsetHeight || 0;
+                            const divHeight = 404 + desHeight;
+                            this.multiSubjectData.chartPagesNum = this.SingleChartPage(this.multiSubjectData.datay.length > 0 ? divHeight : 0);
+                        }else{
+                            this.multiSubjectData.chartPagesNum = this.SingleChartPage(0);
+                        }
+                        // console.log(divHeight,desHeight,this.multiSubjectData.chartPagesNum,13322133)
+                    })
+                } else {
+                    this.multiSubjectData.studentOpenness = '';
+                    this.multiSubjectData.showHistory = 0;
+                    this.multiSubjectData.tablePagesNum = [];
+                    this.multiSubjectData.tableList = [];
+                    this.multiSubjectData.headerList = [];
+                    this.multiSubjectData.singleSubject = [];
+                    // 标准分分析
+                    this.multiSubjectData.datax = []
+                    this.multiSubjectData.datay = []
+                    this.multiSubjectData.tooltipData = []
+                    this.multiSubjectData.standardScoreAnalysisStatus = 1;
+                    this.multiSubjectData.maxSubject = []
+                    this.multiSubjectData.minSubject = []
+                    this.multiSubjectData.chartPagesNum = '';//标准分分析图分页
+                }
+            })
+        },
+        //总分,多科历次信息
+        async QueryHistoryExamData() {
+            await this.$api.reportStudent[getApiName()].queryHistoryExamData({
+                ...this.reportParam,
+                subjectGroupType: 1, // 科目是否为组合
+                isTotal: 1, //是否为总分科目 1为总分 0为非总分
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const detailData = (res.data?.detailData || []).reverse();
+                    this.historyExamData.chartData = detailData;
+                    const selectNames = res.data.selectNames || [];
+                    const selectVal = selectNames?.[0]?.prop ?? '';
+                    this.historyExamData.datax = [];
+                    let datay = [],tooltipData = [];
+                    detailData.forEach(item => {
+                        this.historyExamData.datax.push(item.examName)
+                        datay.push(item[selectVal])
+                        tooltipData.push({
+                            name: selectNames?.[0]?.name ?? '',
+                            value: item[selectVal]
+                        })
+                    })
+                    this.historyExamData.datay = [datay];
+                    this.historyExamData.tooltipData = [tooltipData];
+                    this.historyExamData.pageNum = this.SingleChartPage(this.historyExamData.chartData.length > 0 ? 363 : 0);
+                } else {
+                    this.historyExamData.chartData = [];
+                    this.historyExamData.tooltipData = [];
+                    this.historyExamData.datax = [];
+                    this.historyExamData.datay = [];
+                    this.historyExamData.pageNum = '';
+                }
+            })
+        },
+        // 学生端查询总分,多科总结建议信息(联考)
+        async QuerySuggestionData() {
+            await this.$api.reportStudent[getApiName()].querySuggestionData({
+                ...this.reportParam,
+                subjectGroupType: 1, // 科目是否为组合
+                isTotal: 1, //是否为总分科目 1为总分 0为非总分
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const data = res.data;
+                    const upSubjectList = data.upSubjectData || [];
+                    const downSubjectList = data.downSubjectData || [];
+                    const upSubjectData = upSubjectList.map(item => {
+                        if (data.studentOpenness == 1 || data.studentOpenness == 2) {
+                            return `${item.subjectName}(得分${item.score})`
+                        } else {
+                            return `${item.subjectName}(${item.score})`
+                        }}).join('、')
+                    const downSubjectData = downSubjectList.map(item => {
+                        if (data.studentOpenness == 1 || data.studentOpenness == 2) {
+                            return `${item.subjectName}(得分${item.score})`
+                        } else {
+                            return `${item.subjectName}(${item.score})`
+                        }}).join('、')
+                    if(data?.fullScore && data?.fullScore!='-'){
+                        const scoreText = data?.studentOpenness == 1 || data?.studentOpenness == 2 ? '总分' : '总分标准分为'
+                        const unit = data?.studentOpenness == 1 || data?.studentOpenness == 2 ? '分' : '';
+                        this.suggestionHtml = `${data?.studentName || ''}同学,本次考试${scoreText}<span style="color: #2e64fa">${data?.fullScore}</span>${unit},整体处于${data?.summarySuggestionLevel},`;
+                        if(upSubjectData){
+                            this.suggestionHtml += `<span style="color: #3ba272">${upSubjectData}</span>是你的优势学科,建议通过提分练习进行强化,继续保持这类学科的优势性${downSubjectData ? ';' : '。'}`;
+                        }
+                        if(downSubjectData){
+                            this.suggestionHtml += `<span style="color: #f56c6c">${downSubjectData}</span>是你的劣势学科,建议先加强学习,熟练掌握薄弱知识点的基础,然后通过提分练习进行巩固和强化,争取下次考试获得更优异的成绩!`;
+                        }
+                    }else{
+                        this.suggestionHtml = null
+                    } 
+                } else {
+                    this.suggestionHtml = null
+                }
+                this.$nextTick(()=>{
+                    const suggestionHeight = this.$refs?.multiSuggestionModule?.offsetHeight || 0;
+                    // console.log(suggestionHeight,99888)
+                    this.multiSuggestionPageNum = this.SingleChartPage(this.suggestionHtml?suggestionHeight:0);
+                })
+            })
+        },
+        //学生端查询单科-我的成绩
+        async QueryOneSubjectData(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectData({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const headData = [{
+                        name:'班级',
+                        prop:'className',
+                        display:true
+                    },{
+                        name:'原始分',
+                        prop:'score',
+                        display:res?.data?.scoreStatus === 0 //0-显示,1-不显示
+                    },{
+                        name:'赋分',
+                        prop:'rateScore',
+                        display:res?.data?.rateScoreStatus === 0 //0-显示,1-不显示
+                    },{
+                        name:'班排',
+                        prop:'classRank',
+                        display:res?.data?.classRankStatus === 0
+                    },{
+                        name:'年排',
+                        prop:'schoolRank',
+                        display:res?.data?.schoolRankStatus === 0
+                    },{
+                        name:'联排',
+                        prop:'examRank',
+                        display:res?.data?.examRankStatus === 0 && this.reportParam.examLevel == 1 && this.schoolType == 2
+                    },{
+                        name:'区排',
+                        prop:'regionRank',
+                        display:res?.data?.regionRankStatus === 0
+                    },{
+                        name:'赋分等级',
+                        prop:'rateScoreName',
+                        display:res?.data?.rateScoreStatus === 0
+                    },{
+                        name:'标准分',
+                        prop:'standardScore',
+                        display:(res?.data?.standardScoreStatus === 0 && this.schoolType == 2) || this.schoolType == 1
+                    },{
+                        name:'学业等级',
+                        prop:'gradeName',
+                        display:(res?.data?.gradeNameStatus === 0 && this.schoolType == 2) || this.schoolType == 1
+                    },{
+                        name:'得分率',
+                        prop:'scoreRate',
+                        display:res?.data?.scoreRateStatus === 0
+                    },{
+                        name:'班级最高分',
+                        prop:'classMaxScore',
+                        display:res?.data?.classMaxScoreStatus === 0 && this.schoolType == 2
+                    },{
+                        name:'年级最高分',
+                        prop:'gradeMaxScore',
+                        display:res?.data?.gradeMaxScoreStatus === 0 && this.schoolType == 2
+                    },{
+                        name:'联校最高分',
+                        prop:'examMaxScore',
+                        display:res?.data?.examMaxScoreStatus === 0 && this.schoolType == 2
+                    },{
+                        name:'班级均分',
+                        prop:'classAvgScore',
+                        display:res?.data?.classAvgScoreStatus === 0 && this.schoolType == 2
+                    },{
+                        name:'年级均分',
+                        prop:'gradeAvgScore',
+                        display:res?.data?.gradeAvgScoreStatus === 0 && this.schoolType == 2
+                    },{
+                        name:'联校均分',
+                        prop:'examAvgScore',
+                        display:res?.data?.examAvgScoreStatus === 0 && this.schoolType == 2
+                    }]
+                    const groupClassRanks = res.data.groupClassRanks || [];
+                    const groupClassMaxScoreList = res.data.groupClassMaxScoreList || [];
+                    const groupClassAvgScoreList = res.data.groupClassAvgScoreList || [];
+                    groupClassRanks.forEach(item=>{
+                        headData.push({
+                            name:`${item.classGroupName}排名`,
+                            prop:'groupClassRanks',
+                            code:item.classGroupCode,
+                            display:item.classGroupRankStatus === 1
+                        })
+                    })
+                    groupClassMaxScoreList.forEach(item=>{
+                        headData.push({
+                            name:`${item.classGroupName}最高分`,
+                            prop:'groupClassMaxScoreList',
+                            code:item.classGroupCode,
+                            display:item.classGroupResultScoreStatus === 1
+                        })
+                    })
+                    groupClassAvgScoreList.forEach(item=>{
+                        headData.push({
+                            name:`${item.classGroupName}均分`,
+                            prop:'groupClassAvgScoreList',
+                            code:item.classGroupCode,
+                            display:item.classGroupResultScoreStatus === 1
+                        })
+                    }) 
+                    const tableData = [res.data] || [];
+                    const headerList = headData.filter(item=>item.display);
+                    const pageTableData = this.TableRowAndColumnPage(headerList,tableData,8,1);//8列一个表
+                    this.singleSubjectData[index].scrolTablePagesNum = pageTableData.tablePagesNum;
+                    this.singleSubjectData[index].scrolTableList = pageTableData.tableList;
+                    this.singleSubjectData[index].scrolHeaderList = pageTableData.headerList;//表头
+                    this.singleSubjectData[index].showHistory = res?.data?.showHistory || 0;
+                } else {
+                    this.singleSubjectData[index].scrolTablePagesNum = [];
+                    this.singleSubjectData[index].scrolTableList = [];
+                    this.singleSubjectData[index].scrolHeaderList = [];//表头
+                    this.singleSubjectData[index].showHistory = 0;
+                }
+            })
+        },
+        //学生端查询单科-小题分析(表格-图表)
+        async QueryOneSubjectSmallQuestionData(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectSmallQuestionData({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(res => {
+                if (res.code == 200 && res?.data?.tableData?.length > 0) {
+                    const tableData = res.data.tableData || [];
+                    const titleData = res.data.titleData || [];
+                    const studentOpenness = res.data?.studentOpenness ?? '';//控制得分显示对错或值
+                    this.TableChartData(tableData, titleData, studentOpenness, index,'smallQuestionData','小题',subjectCode);
+                }
+            })
+        },
+        //学生端查询单科-大题分析,知识点分析,能力要素分析(联考)
+        async QueryOneSubjectGroupQuestionData(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectGroupQuestionData({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(async res => {
+                if (res.code == 200 && res.data) {
+                    const { bigQuestion, knowledgePointQuestion, abilityQuestion } = res.data
+                    if (bigQuestion && bigQuestion?.tableData?.length > 0) {
+                        const tableData = bigQuestion.tableData || [];
+                        const titleData = bigQuestion.titleData || [];
+                        const studentOpenness = bigQuestion?.studentOpenness ?? '';
+                        this.TableChartData(tableData, titleData, studentOpenness, index,'bigQuestionData','大题',subjectCode);
+                    }
+                    if (knowledgePointQuestion && knowledgePointQuestion?.tableData?.length > 0) {
+                        const tableData = knowledgePointQuestion.tableData || [];
+                        const titleData = knowledgePointQuestion.titleData || [];
+                        const studentOpenness = knowledgePointQuestion?.studentOpenness ?? '';
+                        this.TableChartData(tableData, titleData, studentOpenness, index,'knowledgePointQuestionData','知识点',subjectCode);
+                        if(this.schoolType == 2){
+                            // 等待 DOM 更新完成后获取高度
+                            await this.$nextTick();
+                            await new Promise(resolve => setTimeout(resolve, 2000)); // 额外延迟确保浏览器挂载
+                            //处理知识点分层分页
+                            const refName = `knowLedgeLayering_${subjectCode}`;
+                            const domHeight = this.$refs[refName]?.[0]?.offsetHeight || 0;
+                            this.knowLedgeLayeringPageNum.push(this.SingleChartPage(domHeight));
+                        }
+                    }
+                    if (abilityQuestion && abilityQuestion?.tableData?.length > 0) {
+                        const tableData = abilityQuestion.tableData || [];
+                        const titleData = abilityQuestion.titleData || [];
+                        const studentOpenness = abilityQuestion?.studentOpenness ?? '';
+                        this.TableChartData(tableData, titleData, studentOpenness, index,'abilityQuestionData','能力要素',subjectCode);
+                    }
+                }
+            })
+        },
+        //学生端查询单科-自定义分组(联考)
+        async QueryOneSubjectCustomGroupQuestion(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectCustomGroupQuestion({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const customQuestionData = res?.data?.customQuestionData ?? []
+                    if (customQuestionData && customQuestionData.length > 0) {
+                        customQuestionData.forEach((item, key) => {
+                            const tableData = item?.questionData?.tableData || []
+                            const titleData = item?.questionData?.titleData || []
+                            const studentOpenness = res.data?.studentOpenness ?? ''
+                            this.TableChartData(tableData, titleData, studentOpenness, index,`customQuestionData${key}`,item.customName,subjectCode);
+                        })
+                    }
+                }
+            })
+        },
+        //学生端查询单科-历次查询(联考)
+        async QueryOneSubjectHistoryExamData(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectHistoryExamData({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const detailData = (res.data?.detailData || []).reverse();
+                    // console.log(this.singleSubjectData[index],98899)
+                    this.singleSubjectData[index].historyExamData.chartData = detailData;
+                    const selectNames = res.data.selectNames || [];
+                    const selectVal = selectNames?.[0]?.prop ?? '';
+                    this.singleSubjectData[index].datax = [];
+                    let datay = [],tooltipData = [];
+                    detailData.forEach(item => {
+                        this.singleSubjectData[index].historyExamData.datax.push(item.examName)
+                        datay.push(item[selectVal])
+                        tooltipData.push({
+                            name: selectNames?.[0]?.name ?? '',
+                            value: item[selectVal]
+                        })
+                    })
+                    this.singleSubjectData[index].historyExamData.datay = [datay];
+                    this.singleSubjectData[index].historyExamData.tooltipData = [tooltipData];
+                    this.singleSubjectData[index].historyExamData.pageNum = this.SingleChartPage(this.singleSubjectData[index].historyExamData.chartData.length > 0 ? 363 : 0);
+                }
+            })
+        },
+        //学生端查询单科-总结建议
+        async QueryOneSubjectSuggestionData(subjectCode,index) {
+            await this.$api.reportStudent[getApiName()].queryOneSubjectSuggestionData({
+                ...this.reportParam,
+                subjectGroupType: 0, // 科目是否为组合
+                isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                subjectCode:subjectCode
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    const data = res.data
+                    //* 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
+                    //* 3-得分显示对错,小题分显示分数,4-得分显示对错,小题分显示对错
+                    //* 5-得分显示等级,小题分显示分数,6-得分显示等级,小题分显示对错
+                    const upSubjectList = data?.upSubjectData || [];
+                    const downSubjectList = data?.downSubjectData || [];
+                    const upSubjectData = upSubjectList.map(item => {
+                        if (data.studentOpenness == 1 || data.studentOpenness == 2) {
+                            return `${item.subjectName}得分<span style="color: #3BA272;">${item.score}</span>分`
+                        } else {
+                            return `${item.subjectName}标准分为<span style="color: #3BA272;">${item.score}</span>`
+                        }
+                    }).join('、')
+                    const downSubjectData = downSubjectList.map(item => {
+                        if (data.studentOpenness == 1 || data.studentOpenness == 2) {
+                            return `${item.subjectName}得分<span style="color: #EE6666;">${item.score}</span>分`
+                        } else {
+                            return `${item.subjectName}标准分为<span style="color: #EE6666;">${item.score}</span>`
+                        }
+                    }).join('、')
+                    this.singleSubjectData[index].suggestionHtml = '';
+                    if (data.studentName && (upSubjectData || downSubjectData)) {
+                        this.singleSubjectData[index].suggestionHtml = `${data.studentName}同学,本次考试`
+                    }
+                    if (upSubjectData) {
+                        this.singleSubjectData[index].suggestionHtml += `${upSubjectData},是你的优势学科,建议通过提分练习进行强化,继续保持这类学科的优势性!`
+                    }
+                    if (downSubjectData) {
+                        this.singleSubjectData[index].suggestionHtml += `${downSubjectData},是你的劣势学科,建议先加强学习,熟练掌握薄弱知识点的基础,然后通过提分练习进行巩固和强化,争取下次考试获得更优异的成绩!`
+                    }
+                    this.$nextTick(()=>{
+                        const suggestionHeight = this.$refs?.[`singleSubjectSuggestion_${index}`]?.[0]?.offsetHeight || 0;
+                        // console.log(suggestionHeight,8999111)
+                        this.singleSubjectData[index].suggestionPageNum = this.SingleChartPage(this.singleSubjectData[index].suggestionHtml?suggestionHeight:0);
+                    })
+                } else {
+                    this.singleSubjectData[index].suggestionHtml = ''
+                }
+            })
+        },
+        //答题卡
+        async FindStudentCard(subjectCode,index){
+            await this.$api.reportStudent[getApiName()].findStudentCard({
+                examId:this.reportParam.examId,
+                subjectCode:subjectCode,
+                registrationCode:this.reportParam.registrationCode
+            }).then(res => {
+                if (res.code == 200 && res.data) {
+                    let paperImageList = res.data.pageVOS || [];
+                    //先添加总分数据
+                    let totalScore = {
+                        questionName: '总分',
+                        fullScore: res?.data?.fullScore || '',
+                        score: res?.data?.levelName ?? res?.data?.totalScore,
+                        displayType: res.data.displayType, //显示类型 0-分数 1-对错 2-等级
+                        displayName: res.data.displayName, //显示值
+                        correctType: res.data.correctType, //显示对错的时候 0-错 1-半对 2-全对
+                        questionAnswer: '',
+                        answer: '',
+                        samplingPosition: '{"x":195,"y":247,"page":1}'
+                    }
+                    if (paperImageList.length > 0 && paperImageList[0].questionVOS) {
+                        paperImageList[0].questionVOS.unshift(totalScore)
+                    }
+                    this.singleSubjectData[index].paperImageList = paperImageList;
+                    this.singleSubjectData[index].usedCardType = res?.data?.usedCardType ?? 1;
+                    for(let i = 0;i<paperImageList.length;i++){
+                        const pageNum = this.SingleChartPage(this.printPageHeight);
+                        this.singleSubjectData[index].paperImagePageNum.push(pageNum);
+                    }
+                }
+            })
+        },
+        //处理数据
+        async TableChartData(tableData, titleData, studentOpenness, index,type,groupName,subjectCode) {
+            //柱状图
+            let datax = [],datay = [],radarChart = []
+            tableData.forEach(item => {
+                datax.push(item.questionName)
+                const scoreRate = item?.scoreRate ?? ''
+                datay.push(scoreRate)
+                radarChart.push([item.questionName, scoreRate])
+            })
+            //雷达图
+            const radarChartData = [['group', '得分率'], ...radarChart]
+            let chartHeight = 0;//小题分析只显示table 不显示图
+            if(type=='bigQuestionData'){
+                chartHeight = datay.length > 0 ? 363 : 0; //柱状图
+            }else if (type == 'smallQuestionData'){
+                chartHeight = 0;
+            } else{
+                chartHeight = datay.length > 0 ? 393 : 0;//雷达图
+            }
+            const chartPagesNum = this.SingleChartPage(chartHeight);
+            const staticHeader = titleData.slice(0,1);//固定表头
+            const dynamicHeader = titleData.slice(1);//动态分组标头
+            
+            const pageTableData = this.TableRowAndColumnPage(dynamicHeader,tableData,7,1);//8列一个表
+            const tablePagesNum = pageTableData.tablePagesNum;
+            const tableList = pageTableData.tableList;
+            const headerList = pageTableData.headerList;//表头
+            //查询单科-知识点分层分析表(联考)
+            let knowLedgeLayering = null;
+            if(type == 'knowledgePointQuestionData' && this.schoolType== 2){
+                await this.$api.reportStudent[getApiName()].queryStudentKnowLedgeLayering({
+                    ...this.reportParam,
+                    subjectGroupType: 0, // 科目是否为组合
+                    isTotal: 0, //是否为总分科目 1为总分 0为非总分
+                    subjectCode:subjectCode
+                }).then(res => {
+                    if (res.code == 200 && res.data) {
+                        const { breakThrough,develop,fullScore } = res.data;
+                        knowLedgeLayering = {
+                            breakThrough:breakThrough || '',
+                            develop:develop || '',
+                            fullScore:fullScore || ''
+                        }
+                        //知识点
+                        this.knowLedgeLayeringData.push({
+                            subjectCode:subjectCode,
+                            groupName:groupName,
+                            knowLedgeLayering:knowLedgeLayering
+                        })
+                    }else{
+                        knowLedgeLayering = null;
+                    }
+                })
+            }
+            this.singleSubjectData[index].groupQuestionData.push({
+                type:type,
+                groupName:groupName,
+                studentOpenness:studentOpenness,
+                staticHeader:staticHeader,
+                headerList:headerList,
+                tableList:tableList,
+                tablePagesNum:tablePagesNum,
+                chartPagesNum:chartPagesNum,
+                datax:datax,
+                datay:datay,
+                radarChartData:radarChartData,
+                knowLedgeLayering:knowLedgeLayering
+            })
+        },
+        //按照原始顺序累加,当总和超过A4高度1285时,将当前元素放入下一组
+        groupByThreshold(arr, threshold) {
+            const result = [];
+            let currentGroup = [];
+            let currentSum = 0;
+
+            for (let i = 0; i < arr.length; i++) {
+                const num = arr[i];
+
+                // 如果当前组为空,直接添加
+                if (currentGroup.length === 0) {
+                    currentGroup.push(num);
+                    currentSum += num;
+                }
+                // 如果加上当前元素会超过阈值,则创建新组
+                else if (currentSum + num > threshold) {
+                    result.push(currentGroup);
+                    currentGroup = [num];
+                    currentSum = num;
+                }
+                // 否则添加到当前组
+                else {
+                    currentGroup.push(num);
+                    currentSum += num;
+                }
+            }
+
+            // 添加最后一组
+            if (currentGroup.length > 0) {
+                result.push(currentGroup);
+            }
+
+            return result;
+        },
+        /*
+        *对表格横向和竖向分页
+        *dynamicHeader 动态表头
+        *tableData 表格数据
+        *groupSize 动态表头分页大小
+        *headNum 表头行数
+        */
+        TableRowAndColumnPage(dynamicHeader,tableData,groupSize,headNum){
+            /*
+            * 每个模块的高度
+            */
+            const tableTitle = 49;//表格标题高度
+            const tableHeadHeight = 41;//表头高度
+            const tableGap = 49;//表格和图表之间的间距
+
+            //对表格横向分页
+            const groupDynamicHeader = [];
+            for (let i = 0; i < dynamicHeader.length; i += groupSize) {
+                const group = dynamicHeader.slice(i, i + groupSize);
+                //表格补充
+                if(group.length > 0 && group.length!=groupSize && i > 0){
+                    const tdLens = groupSize - group.length;
+                    for(let j = 0; j < tdLens; j ++){
+                        group.push({})
+                    }
+                }
+                groupDynamicHeader.push(group)
+            }
+            let tablePagesNum = [],tableList = [],headerList = [];//当前模块所有table页码 表格  表格高度
+            for (let i = 0; i < groupDynamicHeader.length; i ++) {
+                let itemTablePagesNum = [],itemTableList = [],itemHeadder= [], moduleHeightData = [] ,isShowSplitLine = [];//每个table的页码 表格  表格高度 isShowSplitLine:表格是否显示分割线
+                //计算最后一页模块的高度
+                let lastPageDataSum = 0;
+                const lastPageNum = this.modulePageData.length > 0 ? this.modulePageData[this.modulePageData.length - 1] : 1;//最后一页页码
+                this.modulePageData.forEach((item,p)=>{
+                    if(item == lastPageNum){
+                        lastPageDataSum += this.moduleHeightData[p];
+                    }
+                })
+                const remainHeight = this.printPageHeight - lastPageDataSum - (tableHeadHeight * headNum) - tableTitle - 41;//41 是距离底部的边距 
+                const remainTableRow = Math.floor(remainHeight / 40);//剩余可放多少行
+                if(remainTableRow > 1){
+                    const tableLens = tableData.length ?? 0;//表格行数量
+                    if(tableLens > remainTableRow){//判断第二页是否有值
+                        const pageTableHeight = this.printPageHeight - (tableHeadHeight * headNum) - tableTitle - 41;// 一页能放下的table的高度  41 是距离底部的边距 
+                        const tableRowNum = Math.floor(pageTableHeight / 40);//一页可放多少行
+                        //第一页数据
+                        const firstTableData = tableData.slice(0,remainTableRow);
+                        itemTableList.push(firstTableData)
+                        itemHeadder.push(groupDynamicHeader[i])
+                        isShowSplitLine.push(false);//是否显示表格底部的分割线 第一页不显示
+                        //第一页页码
+                        // this.modulePageData.push(lastPageNum);
+                        //当前表格第一页页码
+                        itemTablePagesNum.push(lastPageNum);//图表分页页码
+                        // 第一页高度
+                        const tableHeight1 = firstTableData.length > 0 ? (tableHeadHeight * headNum) + tableTitle + tableGap + firstTableData.length * 40 : 0;
+                        moduleHeightData.push(tableHeight1)
+                        const otherTableData = tableData.slice(remainTableRow);//接取完上一页剩下的表格数据
+                        // 按步长遍历,每次截取 表格行数 元素
+                        for (let j = 0; j < otherTableData.length; j += tableRowNum) {
+                            const group = otherTableData.slice(j, j + tableRowNum);
+                            const key = j/tableRowNum;
+                            //表格数据
+                            itemTableList.push(group);
+                            itemHeadder.push(groupDynamicHeader[i])
+                            isShowSplitLine.push(false);//是否显示表格底部的分割线
+                            //页码
+                            // this.modulePageData.push(lastPageNum + key + 1);
+                            //当前表格分页页码
+                            itemTablePagesNum.push(lastPageNum + key + 1);
+                            //高度
+                            const tableHeight = group.length > 0 ? (tableHeadHeight * headNum) + tableTitle + tableGap + group.length * 40 : 0;
+                            moduleHeightData.push(tableHeight)
+                        }
+                    }else{
+                        const tableData1 = tableData.slice(0,remainTableRow);
+                        const tableData2 = tableData.slice(remainTableRow);
+                        itemTableList.push(tableData1,tableData2);
+                        itemHeadder.push(groupDynamicHeader[i],groupDynamicHeader[i])
+                        // this.modulePageData.push(lastPageNum,lastPageNum);
+                        //页码
+                        itemTablePagesNum.push(lastPageNum,lastPageNum)
+                        //高度
+                        const tableHeight1 = tableData1.length > 0 ? (tableHeadHeight * headNum) + tableTitle + tableGap + tableData1.length * 40 : 0;
+                        const tableHeight2 = tableData2.length > 0 ? (tableHeadHeight * headNum) + tableTitle + tableGap + tableData2.length * 40 : 0;
+                        moduleHeightData.push(tableHeight1,tableHeight2)
+                    }
+                }else{
+                    const pageTableHeight = this.printPageHeight - (tableHeadHeight * headNum) - tableTitle - 41;// 一页能放下的table的高度  41 是距离底部的边距 
+                    const tableRowNum = Math.floor(pageTableHeight / 40);//一页可放多少行
+                    // 按步长遍历,每次截取 表格行数 元素
+                    for (let j = 0; j < tableData.length; j += tableRowNum) {
+                        const group = tableData.slice(j, j + tableRowNum);
+                        const key = j/tableRowNum;
+                        //表格数据
+                        itemTableList.push(group);
+                        itemHeadder.push(groupDynamicHeader[i])
+                        // this.modulePageData.push(lastPageNum + key + 1);
+                        //页码
+                        itemTablePagesNum.push(lastPageNum + key + 1)
+                        //高度
+                        const tableHeight = group.length > 0 ? (tableHeadHeight * headNum) + tableTitle + tableGap + group.length * 40 : 0;
+                        moduleHeightData.push(tableHeight)
+                    }
+                }
+                this.modulePageData.push(...itemTablePagesNum);
+                this.moduleHeightData.push(...moduleHeightData);
+                // console.log(this.modulePageData,111333)
+                tableList.push(itemTableList)
+                headerList.push(itemHeadder)
+                tablePagesNum.push(itemTablePagesNum);//当前模块表格的分页页码
+            }
+            return {headerList,tablePagesNum,tableList}
+        },
+        /*
+        *单个图分页
+        *chartHeight 图表高度
+        */
+        SingleChartPage(chartHeight){
+            let chartPagesNum = '';//各分析图 高度
+            const prevModuleLastPageNum = this.modulePageData.length > 0 ? this.modulePageData[this.modulePageData.length - 1] : 1;//上一模块 最后一页页码
+            //上一模块最后一页高度 和 当前模块 echart高度
+            let prevModuleLastPageHeight = [],moduleChartHeight = [];
+            this.modulePageData.forEach((item,i)=>{
+                if(item == prevModuleLastPageNum){
+                    prevModuleLastPageHeight.push(this.moduleHeightData[i])
+                    moduleChartHeight.push(this.moduleHeightData[i]);//最后一页高度
+                }
+            })
+            // 将当前模块 echart高度
+            this.moduleHeightData.push(chartHeight)
+            moduleChartHeight.push(chartHeight)
+            //进行分页 满足一页的高度放在一起
+            const printPageGroup = this.groupByThreshold([...moduleChartHeight], this.printPageHeight);
+            //每个模块对应的页码
+            for (let i = 0; i < printPageGroup.length; i++) {
+                const arr = printPageGroup[i]
+                for (let j = 0; j < arr.length; j++) {
+                    if(i == 0){//第一页
+                        if(j > prevModuleLastPageHeight.length - 1){
+                            this.modulePageData.push(prevModuleLastPageNum)
+                            chartPagesNum = prevModuleLastPageNum
+                        }
+                    }else{
+                        this.modulePageData.push(prevModuleLastPageNum + i);
+                        chartPagesNum = prevModuleLastPageNum + i
+                    }
+                }
+            }
+            return chartPagesNum
+        },
+        GetDifficultyClass(val) {
+            if (val == 1) {
+                return 'difficulty easy'
+            } else if (val == 2) {
+                return 'difficulty relatively_easy'
+            } else if (val == 3) {
+                return 'difficulty general'
+            } else if (val == 4) {
+                return 'difficulty more_difficult'
+            } else if (val == 5) {
+                return 'difficulty difficult'
+            } else {
+                return ''
+            }
+        },
+        GetDifficultyName(val) {
+            if (val == 1) {
+                return '容易'
+            } else if (val == 2) {
+                return '较易'
+            } else if (val == 3) {
+                return '一般'
+            } else if (val == 4) {
+                return '较难'
+            } else if (val == 5) {
+                return '困难'
+            } else {
+                return '-'
+            }
+        },
+        GetGroupClassValue(data,prop,code){
+            const obj = data.find(item=>item.classGroupCode==code);
+            const val = prop=='groupClassRanks'?obj?.classGroupRankRate:obj?.groupClassResultScore;
+            return val ?? '-'
+        },
+        //点击顶部下载PDF按钮导出
+        DownloadPdf(){
+            if(this.$refs.bookFlipBox){
+                this.$refs.bookFlipBox.DownloadPdfNew();
+            }
+        },
+        //向父级页面传值关闭下载Pdf按钮loading
+        PdfLoadEnd(){
+            this.$emit('closePdfLoading')
+        },
+    }
+};
+</script>
+<style scoped lang="scss">
+    .area_module_describe{
+        margin-top: 20px;
+    }
+    .card-container {
+        display: flex;
+        flex-wrap: wrap;
+        /* 允许换行 */
+        /* 设置元素之间间隔 */
+        gap: 10px;
+        .card {
+            //   flex: 0 1 calc((100% - 240px) / 5);
+            flex-grow: 1; /* 使每个div平分可用空间 */
+            flex-basis: 0; /* 初始基础大小为0 */
+            width: 20%;
+            //   max-width: 320px;
+            // width: 238px;
+            /* 每个卡片占据 18% 宽度,确保能在一行显示五个,考虑间隔 */
+            // flex: 0 0 16.5%; /* 每个卡片占据 18% 宽度,确保能在一行显示五个,考虑间隔 */
+            // border: 2px dashed #ccc;
+            border-radius: 10px;
+            padding: 12px 6px;
+            box-sizing: border-box;
+            text-align: center;
+            color: #fff;
+            /* 文字颜色可以根据背景调整 */
+            background-size: cover;
+            background-position: center;
+            height: 70px;
+            /* 可调整高度 */
+            // margin-bottom: 20px; /* 卡片底部间距 */
+            position: relative;
+        }
+        .background-image {
+            position: absolute;
+            top: 0;
+            right: 0;
+            /* 背景图片宽度 */
+            width: 100%;
+            height: 70px;
+            background-size: contain;
+            background-repeat: no-repeat;
+            background-position: center right;
+            z-index: 1;
+            /* 确保背景图片在文本之下 */
+            border-radius: 10px;
+        }
+        .statistic,.value {
+            position: relative;
+            z-index: 2;
+            /* 确保文本在背景图片之上 */
+        }
+        .statistic {
+            font-size: 13px;
+            font-weight: 400;
+            line-height: 22px;
+            text-align: left;
+            z-index: 2;
+        }
+        .value {
+            font-size: 16px;
+            font-weight: 600;
+            line-height: 20px;
+            text-align: left;
+            margin-top: 5px;
+            z-index: 2;
+        }
+    }
+    .area_module{
+        .area_module_content{
+            display: flex;
+            width: 100%;
+            justify-content: space-between;
+            .area_module_chart{
+                width: 35% !important;
+            }
+            .area_module_table{
+                width: 65% !important;
+            }
+        }
+        .area_module_chart{
+            &.justify_content{
+                display: flex;
+                gap: 0px 48px;
+                flex-wrap: wrap;
+                .item_progress{
+                    width: 170px;
+                    height: 180px;
+                    display: inline-flex;
+                    align-items: center;
+                    position: relative;
+                    :deep(.el-progress--circle) {
+                        .el-progress__text{
+                            font-size: 14px !important;
+                            color: #5470C6 !important;
+                            background: rgba(84,112,198,0.1);
+                            width: 120px;
+                            height: 120px;
+                            border-radius: 50%;
+                            position: absolute;
+                            left: 50%;
+                            top: 50%;
+                            transform: translate(-50%, -50%);
+                            display: flex;
+                            align-items: center;
+                            justify-content: center;
+                            padding-top: 30px;
+                            box-sizing: border-box;
+                        }
+                    }
+                    .item_progress_name{
+                        position: absolute;
+                        left: 50%;
+                        top: 37%;
+                        transform: translate(-50%,0);
+                        font-weight: 500;
+                        font-size: 16px !important;
+                        color: #5470C6 !important;
+                    }
+                }
+            }
+        }
+        .area_module_table{
+            &.error{
+                :deep() .el-table{
+                    .el-table__header-wrapper{
+                        .el-table__header{
+                            .is-group{
+                                tr{
+                                    th.el-table__cell:nth-last-child(3){
+                                        border-right: 1px solid #EBEEF5;
+                                    }
+                                    &:nth-child(2){
+                                        display:none;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    .el-table__cell{
+                        &.white_space_normal{
+                            .cell{
+                                white-space: normal;
+                                padding:10px 0 10px 10px !important;
+                            }
+                        }
+                    }
+                    // &.el-table--striped {
+                    //     .el-table__body {
+                    //         tr{
+                    //             &.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:hover > td {
+                    //     background-color: transparent !important;
+                    // }
+                }
+            }
+            .student_know_paper{
+                width: 100%;
+                box-sizing: border-box;
+                display: flex;
+                flex-direction: column;
+                border-radius: 6px;
+                border: 1px solid #EBEEF5;
+                li{
+                    width: 100%;
+                    border-top: 1px solid #EBEEF5;
+                    box-sizing: border-box;
+                    display: inline-flex;
+                    &:first-child{
+                        border-top: 0;
+                        .li_left{
+                            background-color: #EEF1FB;
+                            span{
+                                color: #5470C6;
+                            }
+                        }
+                    }
+                    &:nth-child(2){
+                        .li_left{
+                            background-color: #ECF6F1;
+                            span{
+                                color: #3BA272;
+                            }
+                        }
+                    }
+                    &:nth-child(3){
+                        .li_left{
+                            background-color: #FFFAEF;
+                            span{
+                                color: #FAC858;
+                            }
+                        }
+                    }
+                    .li_left{
+                        width: 300px;
+                        padding: 24px;
+                        border-right: 1px solid #EBEEF5;
+                        box-sizing: border-box;
+                        display: inline-flex;
+                        flex-direction: column;
+                        justify-content: center;
+                        flex-shrink: 0;
+                        span{
+                            font-weight: 500;
+                            font-size: 20px;
+                            line-height: 28px;
+                            display: block;
+                            text-align: center;
+                            &:nth-child(2){
+                                margin-top: 4px;
+                                line-height: 20px;
+                                font-weight: 400;
+                                font-size: 14px;
+                                color: #999999;
+                            }     
+                        }
+                    }
+                    .li_know{
+                        flex: 1;
+                        padding: 20px;
+                        box-sizing: border-box;
+                        font-weight: 400;
+                        font-size: 14px;
+                        color: #666666;
+                        line-height: 24px;
+                        text-align: justified;
+                        display: inline-flex;
+                        align-items: center;
+                    }
+                }
+            }
+        }
+        .difficulty {
+            width: 6px;
+            height: 6px;
+            display: inline-flex;
+            border-radius: 50%;
+            margin-right: 4px;
+            &.easy {
+                background: #3ba272;
+            }
+            &.relatively_easy {
+                background: #fac858;
+            }
+            &.general {
+                background: #5470c6;
+            }
+            &.more_difficult {
+                background: #ea7acb;
+            }
+            &.difficult {
+                background: #ee6666;
+            }
+        }
+    }
+</style>

+ 7 - 4
src/views/analysisReport/studentPage/list/mainPage.vue

@@ -48,6 +48,11 @@
                 }">{{ course.subjectName }}</span>
               </el-tooltip>
             </span>
+            <span class="paper_remark" v-if="course.subjectNotes">
+              <el-tooltip :disabled="!course.subjectNotes || course.subjectNotes.length<=4" placement="top" :content="course.subjectNotes">
+                <span :style="{'color': GetSubjectsColor(course.subjectName, course.subjectNum), 'border-color': GetSubjectsColor(course.subjectName,course.subjectNum)}">{{ course.subjectNotes }}</span>
+              </el-tooltip>
+            </span>
           </div>
         </div>
       </div>
@@ -63,7 +68,7 @@
 </template>
 <script>
 import FiltersItem from "@/components/FiltersItem_ruoyan.vue";
-
+import {getApiName} from '@/utils/common';
 export default {
   components: { FiltersItem },
   data() {
@@ -283,9 +288,7 @@ export default {
         examTypeCode: this.params.examType, // 考试类型
       };
       // 获取考试分析列表数据
-      this.$api.reportStudent
-        .queryStudentExamDataList(param)
-        .then((res) => {
+      this.$api.reportStudent[getApiName()].queryStudentExamDataList(param).then((res) => {
           if (res.code === 200) {
             this.listData = res?.data?.examData || [];
             this.pageInfo.total = this.listData.length;

+ 95 - 20
src/views/analysisReport/studentPage/mainPage.vue

@@ -1,7 +1,7 @@
 <template>
     <!-- 分析报告详情总页面 -->
     <div class="analysis_main">
-        <div class="main_header" ref="mainHeader">
+        <div :class="['main_header', { 'full_screen': isLianXiao }]" ref="mainHeader">
             <div class="header_left">
                 <span class="back_button" @click="GoBack">
                     <i class="iconfont icon_return"></i>返回
@@ -10,46 +10,56 @@
             </div>
             <div class="header_right">
                 <div class="select_list" v-if="isShowFilter">
-                    <el-select style="width: 120px" v-model="item.value" :placeholder="'请选择' + item.name"
-                        class="select_item" @change="ChangeFilters({ index: index, value: item.value })"
-                        v-for="(item, index) in filterData" :key="index" v-show="item.list.length > 1">
-                        <el-option v-for="item in item.list" :key="item.value" :label="item.label"
-                            :value="item.value"></el-option>
+                    <el-select style="width: 120px" v-model="filterData[index].value" :placeholder="'请选择' + item.name"
+                        class="select_item" @change="ChangeFilters({ index: index, value: $event })"
+                        v-for="(item, index) in filteredFilterData" :key="index" v-show="item.list.length > 1">
+                        <el-option v-for="option in item.list" :key="option.value" :label="option.label"
+                            :value="option.value"></el-option>
                     </el-select>
                 </div>
                 <template v-if="isShowBtn">
-                    <el-button size="medium" @click="downloadWrongQuestions(0)">下载错题本</el-button>
-                    <el-button type="primary" size="medium" @click="downloadWrongQuestions(1)">下载个性化提升手册</el-button>
+                    <el-button size="medium" :disabled="!canBtnClick"
+                        @click="downloadWrongQuestions(0)">下载错题本</el-button>
+                    <el-button :disabled="!canBtnClick" type="primary" size="medium"
+                        @click="downloadWrongQuestions(1)">下载个性化提升手册</el-button>
                 </template>
+                <!-- <el-button v-if="isShowPadfBtn" style="margin-left: 10px;" size="medium" type="primary" :loading="stuPdfLoading" @click="StuDownloadPDF">下载PDF</el-button> -->
             </div>
         </div>
 
         <div class="main_content">
             <div class="content_right" ref="rightContent" @scroll="ScrollChange">
-                <div class="content_right_scroll" ref="contentRightScroll">
+                <div :class="['content_right_scroll', { 'full_screen': isLianXiao }]" ref="contentRightScroll">
                     <template v-if="isLianXiao">
                         <div class="mm_body">
                             <div class="left">
                                 <button class="mm_btn mb_10" :class="{ active: activeBtn === pathOne }"
                                     @click="toPage(pathOne)">成绩分析</button>
 
-                                <button class="mm_btn" :class="{ active: activeBtn === pathTwo }"
-                                    @click="toPage(pathTwo)">个性化错题</button>
+                                <button v-if="isShowKnowledgeButtons" class="mm_btn mb_10"
+                                    :class="{ active: activeBtn === pathTwo }" @click="toPage(pathTwo)">举一反三</button>
+                                <!-- <button v-if="!isTotalScore" class="mm_btn mb_10"
+                                    :class="{ active: activeBtn === pathThree }"
+                                    @click="toPage(pathThree)">个人画像</button> -->
                             </div>
+
                             <div class="right">
                                 <div class="page_filter" ref="filterContent">
-                                    <FiltersItem :filtersData="filterData" @selectItem="ChangeFilters"></FiltersItem>
+                                    <FiltersItem :filtersData="filteredFilterData" @selectItem="ChangeFilters">
+                                    </FiltersItem>
                                 </div>
-                                <router-view ref="child"></router-view>
+                                <router-view ref="child" @isPdfDataLoadEnd="isPdfDataLoadEnd"
+                                    @closePdfLoading="closePdfLoading"></router-view>
                             </div>
                         </div>
                     </template>
 
                     <template v-else>
                         <div class="page_filter" ref="filterContent">
-                            <FiltersItem :filtersData="filterData" @selectItem="ChangeFilters"></FiltersItem>
+                            <FiltersItem :filtersData="filteredFilterData" @selectItem="ChangeFilters"></FiltersItem>
                         </div>
-                        <router-view ref="child"></router-view>
+                        <router-view ref="child" @isPdfDataLoadEnd="isPdfDataLoadEnd"
+                            @closePdfLoading="closePdfLoading"></router-view>
                     </template>
                 </div>
             </div>
@@ -72,11 +82,29 @@ export default {
         updateScrollTop() {
             return this.$store.state.report.updateScrollTop;//监听改变滚动条参数 
         },
+        isShowKnowledgeButtons() {
+            // 从环境变量中获取基础URL,判断是否显示按钮
+            return process.env.VUE_APP_BASE !== 'https://www.k12100.com';
+        },
+        // 判断是否为总分科目,用于控制个人画像按钮显示
+        isTotalScore() {
+            return this.$store.state.report.isTotalScore;
+        },
+        // 筛选数据,不再在个人画像页面隐藏总分选项
+        filteredFilterData() {
+            // 返回原始数据,不在个人画像页面隐藏总分选项
+            return this.filterData;
+        },
+        canBtnClick() {
+            return this.$store.state.question.canDownloanBtnClick;
+        }
     },
     data() {
         return {
             isShowFilter: false, //是否显示筛选条件
             isShowBtn: false, //是否显示下载按钮
+            isShowPadfBtn: false,//是否显示下载pdf按钮
+            isPdfLoadEnd: false,//报告册数据是否加载完成
             filterData: [
                 {
                     name: "科目名称",
@@ -89,11 +117,14 @@ export default {
             activeBtn: this.$route.path,
             pathOne: '/studentAnalysisReport/reportDetails/scrolReport',
             pathTwo: '/studentAnalysisReport/reportDetails/personalWrongQuestions',
+            pathThree: '/studentAnalysisReport/reportDetails/personalProfile',//个人画像
             isLianXiao: false,//是否联校
+            stuPdfLoading: false,
         };
     },
 
     created() {
+        this.isShowPadfBtn = this.$route.path == '/studentAnalysisReport/reportDetails/scrolReport';
         const schoolType = sessionStorage.getItem('schoolType') || '1'; //1:单校 2:联校
         this.isLianXiao = schoolType === '2';
 
@@ -125,7 +156,12 @@ export default {
 
         SetHeadWidth() {
             if (this.$refs.contentRightScroll) {
-                const scrollDivWidth = this.$refs.contentRightScroll.offsetWidth;
+                let scrollDivWidth = 0;
+                if (this.isLianXiao) {
+                    scrollDivWidth = this.$refs.contentRightScroll.offsetWidth - 20;//联校
+                } else {
+                    scrollDivWidth = this.$refs.contentRightScroll.offsetWidth;
+                }
                 const headDiv = this.$refs.mainHeader;
                 headDiv.style.width = `${scrollDivWidth}px`;
             }
@@ -133,6 +169,7 @@ export default {
 
         // 筛选事件
         ChangeFilters(e) {
+
             this.filterData[e.index].value = e.value;
             // 选中科目数据
             let courseObj = this.filterData[0].list.find(item => item.value == this.filterData[0].value);
@@ -146,15 +183,20 @@ export default {
                 isTotal: courseObj.isTotal //是否为总分科目 1为总分 0为非总分
             };
             //设置是否是总分
+            const isTotal = courseObj.isTotal == 1 || courseObj.subjectGroupType == 1;//1为总分 0为非总分  1为组合科目 0为非组合科目
             this.$store.commit("report/set_state", {
                 key: "isTotalScore",
-                value: courseObj.isTotal == 1 || courseObj.subjectGroupType == 1,//1为总分 0为非总分  1为组合科目 0为非组合科目
+                value: isTotal,
             });
 
             localStorage.setItem('reportExamCourseId', courseObj.subjectId);//单科的考试科目id
-            localStorage.setItem('reportIsTotalScore', courseObj.isTotal == 1 || courseObj.subjectGroupType == 1)
-            console.log("打印筛选数据", filterObject);
+            localStorage.setItem('reportIsTotalScore', isTotal)
             this.$store.dispatch("report/UpdateFilterObject", filterObject);
+
+            // 如果选择了总分且当前在个人画像页面,跳转到成绩分析页面
+            if (isTotal && this.$route.path === '/studentAnalysisReport/reportDetails/personalProfile') {
+                this.toPage(this.pathOne);
+            }
         },
 
         // 滚动条事件
@@ -200,12 +242,37 @@ export default {
 
         //返回按钮点击
         GoBack() {
-            this.$router.push("/studentAnalysisReport/list");
+            const schoolType = sessionStorage.getItem('schoolType');
+            if (schoolType == 1) {//单校
+                this.$router.push("/studentAnalysisReport/list");
+            } else {//联校
+                this.$router.push("/jointStudentAnalysisReport/list");
+            }
+
+        },
+        StuDownloadPDF() {
+            if (!this.isPdfLoadEnd) {
+                return this.$message.warning('请稍等报告数据生成中!');
+            }
+            this.stuPdfLoading = true;
+            this.$refs.child.DownloadPdf();
+        },
+        isPdfDataLoadEnd() {
+            this.isPdfLoadEnd = true;
+        },
+        closePdfLoading() {
+            this.stuPdfLoading = false;
+        },
+
+        // 处理筛选数据更新
+        handleFilterDataUpdate() {
+            // 不再在个人画像页面隐藏总分选项
         },
     },
     watch: {
         '$route'(to, from) {
             this.ResetScroll();//重置滚动条
+            this.handleFilterDataUpdate();
         },//路由变化 重置页面滚动条位置
 
         updateScrollTop: {
@@ -214,6 +281,14 @@ export default {
             },
         },
 
+        // 监听筛选数据变化,动态过滤总分选项
+        filterData: {
+            deep: true,
+            handler() {
+                this.handleFilterDataUpdate();
+            }
+        }
+
     },
     beforeUnmount() {
         // 解绑事件,避免内存泄漏

+ 19 - 4
src/views/analysisReport/studentPage/scrolReport/transcript.vue

@@ -3,6 +3,7 @@
   <div>
     <TranscriptTotal v-if="isTotalScore" />
     <TranscriptSingle v-else />
+    <!-- <StudentReport ref="studentReportPdf" @closePdfLoading="closePdfLoading" @isPdfDataLoadEnd="isPdfDataLoadEnd"></StudentReport> -->
   </div>
 </template>
 
@@ -10,12 +11,13 @@
 
 import TranscriptSingle from "@/views/analysisReport/studentPage/scrolReport/transcript_single";//单科的G组分析
 import TranscriptTotal from "@/views/analysisReport/studentPage/scrolReport/transcript_total";//单科的G组分析
-
+// import StudentReport from '../downloadPdf/studentReport.vue'
 
 export default {
   components: {
     TranscriptSingle,
-    TranscriptTotal
+    TranscriptTotal,
+    // StudentReport
   },
   computed: {
     isTotalScore(){
@@ -32,8 +34,21 @@ export default {
   },
   mounted() {
   },
-  methods:
-  {
+  methods:{
+    //点击顶部下载PDF按钮导出
+    DownloadPdf(){
+        if(this.$refs.studentReportPdf){
+            this.$refs.studentReportPdf.DownloadPdf();
+        }
+    },
+    //是否显示pdf下载按钮
+    isPdfDataLoadEnd(){
+      this.$emit('isPdfDataLoadEnd')
+    },
+    //向父级页面传值关闭下载Pdf按钮loading
+    closePdfLoading(){
+        this.$emit('closePdfLoading')
+    },
   },
 };
 </script>

+ 192 - 33
src/views/analysisReport/studentPage/scrolReport/transcript_single.vue

@@ -44,12 +44,15 @@
                     (subjectData.data.studentOpenness == 3 || subjectData.data.studentOpenness == 4) && subjectData?.data?.score
                   "
                 >
-                  <img
-                    v-if="subjectData?.data?.fullScore == subjectData?.data?.score"
-                    src="@/assets/stuIcon/icon_right.png"
-                  />
-                  <img v-else-if="subjectData?.data?.score === 0" src="@/assets/stuIcon/icon_error.png" />
-                  <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                  <template v-if="!isNaN(subjectData?.data?.score)">
+                    <img
+                      v-if="subjectData?.data?.fullScore == subjectData?.data?.score"
+                      src="@/assets/stuIcon/icon_right.png"
+                    />
+                    <img v-else-if="subjectData?.data?.score === 0" src="@/assets/stuIcon/icon_error.png" />
+                    <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                  </template>
+                  <template v-else>{{ subjectData?.data?.score ?? '-' }}</template>
                 </template>
                 <template v-else>{{ subjectData?.data?.score ?? '-' }}</template>
               </span>
@@ -66,10 +69,14 @@
               <span class="title">年排</span>
               <span class="value">{{ subjectData?.data?.schoolRank ?? '-' }}</span>
             </div>
-            <div class="score_item" v-if="subjectData?.data?.examRankStatus === 0 && schoolType == 2">
+            <div class="score_item" v-if="subjectData?.data?.examRankStatus === 0 && reportParam.examLevel == 1 && schoolType == 2">
               <span class="title">联排</span>
               <span class="value">{{ subjectData?.data?.examRank ?? '-' }}</span>
             </div>
+            <div class="score_item" v-if="subjectData?.data?.regionRankStatus === 0">
+              <span class="title">区排</span>
+              <span class="value">{{ subjectData?.data?.regionRank ?? '-' }}</span>
+            </div>
             <div class="score_item" v-if="subjectData?.data?.rateScoreStatus === 0">
               <span class="title">赋分等级</span>
               <span class="value">{{ subjectData?.data?.rateScoreName ?? '-' }}</span>
@@ -110,6 +117,28 @@
               <span class="title">联校均分</span>
               <span class="value">{{ subjectData?.data?.examAvgScore ?? '-' }}</span>
             </div>
+            <div class="score_item" v-if="subjectData?.data?.tscoreStatus === 0">
+              <span class="title">T分数</span>
+              <span class="value">{{ subjectData?.data?.tscore ?? '-' }}</span>
+            </div>
+            <template v-for="item in (subjectData?.data?.groupClassRanks || [])">
+              <div class="score_item" v-if="item?.classGroupRankStatus === 1">
+                <span class="title">{{`${item.classGroupName}排名`}}</span>
+                <span class="value">{{ item?.classGroupRank ?? '-' }}</span>
+              </div>
+            </template>
+            <template v-for="item in (subjectData?.data?.groupClassMaxScoreList || [])">
+              <div class="score_item" v-if="item?.classGroupResultScoreStatus === 1">
+                <span class="title">{{`${item.classGroupName}最高分`}}</span>
+                <span class="value">{{ item?.groupClassResultScore ?? '-' }}</span>
+              </div>
+            </template>
+            <template v-for="item in (subjectData?.data?.groupClassAvgScoreList || [])">
+              <div class="score_item" v-if="item?.classGroupResultScoreStatus === 1">
+                <span class="title">{{`${item.classGroupName}均分`}}</span>
+                <span class="value">{{ item?.groupClassResultScore ?? '-' }}</span>
+              </div>
+            </template>
           </div>
         </div>
         <div class="page_jg_20"></div>
@@ -236,11 +265,14 @@
                       groupData[group.value].studentOpenness == 6
                     "
                   >
-                    <img v-if="scope.row.fullScore == scope.row.score" src="@/assets/stuIcon/icon_right.png" />
-                    <img v-else-if="scope.row.score == 0" src="@/assets/stuIcon/icon_error.png" />
-                    <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                    <template v-if="!isNaN(scope.row.score)">
+                      <img v-if="scope.row.fullScore == scope.row.score" src="@/assets/stuIcon/icon_right.png" />
+                      <img v-else-if="scope.row.score == 0" src="@/assets/stuIcon/icon_error.png" />
+                      <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                    </template>
+                    <template v-else>{{ scope.row.score ?? '-' }}</template>
                   </template>
-                  <template v-else>{{ scope.row.score || '-' }}</template>
+                  <template v-else>{{ scope.row.score ?? '-' }}</template>
                 </template>
                 <!-- 包含小题 -->
                 <template
@@ -255,6 +287,7 @@
                   ><span :class="GetDifficultyClass(scope.row[title.prop])"></span
                   ><span>{{ GetDifficultyName(scope.row[title.prop]) }}</span></template
                 >
+                <template v-else-if="title.prop == 'classGroupAvgScoreMap' || title.prop == 'classGroupScoreRateMap'">{{ scope.row?.[title.prop]?.[title.code] || '-' }}</template>
                 <template v-else>{{ scope.row[title.prop] || '-' }}</template>
               </template>
             </el-table-column>
@@ -276,8 +309,39 @@
           <div v-else class="page_jg_20"></div>
         </div>
       </div>
+      <!-- 知识点分层 -->
+      <div class="report_module" v-if="group.value=='knowledgePointQuestionData'">
+        <div class="module_title">
+          <div class="title_left">{{ group.name }}分层分析表</div>
+        </div>
+        <div class="module_table pb20">
+          <ul class="student_know_paper">
+            <li>
+              <div class="li_left">
+                  <span>满分区</span>
+                  <span>(得满分的知识点)</span>
+              </div>
+              <div class="li_know">{{ knowLedgeLayering.fullScore }}{{ knowLedgeLayering.fullScore ? '。':'' }}</div>
+            </li>
+            <li>
+              <div class="li_left">
+                  <span>突破升级区</span>
+                  <span>(得分率高于同层次,但低于上一层次平均水平的知识点)</span>
+              </div>
+              <div class="li_know">{{ knowLedgeLayering.breakThrough }}{{ knowLedgeLayering.breakThrough ? '。':'' }}</div>
+            </li>
+            <li>
+              <div class="li_left">
+                  <span>就近发展区</span>
+                  <span>(得分率低于同层次平均水平的知识点)</span>
+              </div>
+              <div class="li_know">{{ knowLedgeLayering.develop }}{{ knowLedgeLayering.develop ? '。':'' }}</div>
+            </li>
+          </ul>
+        </div>
+      </div>
     </template>
-    <div class="report_module" v-if="false">
+    <div class="report_module" v-if="subjectData?.data?.showHistory == 1">
       <div class="module_title">
         <div class="title_left">历次考试分析</div>
         <div class="title_right report_button">
@@ -342,6 +406,7 @@ import ExpandableText from '@/views/analysisReport/components/ExpandableText' //
 import LineChart from '@/views/analysisReport/components/dCharts/lineChart' //折线图
 import GaugeChart from '@/views/analysisReport/components/dCharts/GaugeChart'
 import GotoTop from '@/views/analysisReport/components/GotoTop' //分析报告页面底部回到顶部组件
+import {getApiName} from '@/utils/common';
 import { mapGetters } from 'vuex'
 export default {
   name: 'subjectQuality',
@@ -446,6 +511,11 @@ export default {
           radarChartData: []
         }
       },
+      knowLedgeLayering:{
+        fullScore:'',
+        develop:'',
+        breakThrough:''
+      },//知识点分层
       subjectLoading: true, //加载状态
       historyExamLoading: false,
       loadingText: '加载中,请稍后……',
@@ -516,6 +586,9 @@ export default {
       this.QueryOneSubjectData() //学生端查询单科-我的成绩
       this.FindStudentCard() //查询学生答题卡带批阅痕迹的(单校)
       this.QueryOneSubjectSmallQuestionData() //学生端查询单科-小题分析(表格-图表)
+      if(this.schoolType == 2){
+        this.QueryStudentKnowLedgeLayering();//知识点分层分析表(联考)
+      }
       this.QueryOneSubjectHistoryExamData() //学生端查询单科-历次查询
       this.QueryOneSubjectSuggestionData() //学生端查询单科-总结建议
     },
@@ -539,13 +612,12 @@ export default {
     //学生端查询单科-我的成绩
     QueryOneSubjectData() {
       this.subjectLoading = true
-      this.$api.reportStudent
-        .queryOneSubjectData(this.reportParam)
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectData(this.reportParam).then(res => {
           if (res.code == 200 && res.data) {
             this.subjectData.data = res.data
-            const academicLevelData = res.data.academicLevelData
-            const examAcademicLevelList = (academicLevelData.examAcademicLevelList || []).reverse()
+            const academicLevelData = res.data.academicLevelData;
+            const levelList = academicLevelData?.examAcademicLevelList || [];
+            const examAcademicLevelList = academicLevelData.scoreType == 1?[...levelList].reverse():[...levelList];
             this.subjectData.levelData = examAcademicLevelList.map(item => ({
               label: item.gradeName,
               range: this.getMiddleNumber(
@@ -553,7 +625,7 @@ export default {
                   ? [Number(item.endScore), Number(item.startScore)]
                   : [Number(item.startScore), Number(item.endScore)]
               ),
-              value: academicLevelData.scoreType == 1 ? Number(item.startScore) : Number(item.Number(item.endScore)) //1 按分数
+              value: academicLevelData.scoreType == 1 ? Number(item.startScore) : Number(item.endScore) //1 按分数
             }))
             const gradeName = this.subjectData.levelData.find(item => item.label == res.data.gradeName)
             this.subjectData.levelValue = gradeName ? gradeName.range : 0
@@ -576,12 +648,10 @@ export default {
     //查询学生答题卡带批阅痕迹的
     FindStudentCard() {
       this.answerCard.loading = true
-      this.$api.reportStudent
-        .findStudentCard({
+      this.$api.reportStudent[getApiName()].findStudentCard({
           examId: this.reportParam.examId, //考试ID
           subjectCode: this.reportParam.subjectCode //科目code
-        })
-        .then(res => {
+        }).then(res => {
           if (res.code == 200 && res.data) {
             this.answerCard.paperImageList = res?.data?.pageVOS || [];
             this.answerCard.studentCode = res?.data?.studentCode ?? ''
@@ -621,9 +691,7 @@ export default {
     //学生端查询单科-小题分析(表格-图表)
     QueryOneSubjectSmallQuestionData() {
       this.groupData.smallQuestionData.loading = true
-      this.$api.reportStudent
-        .queryOneSubjectSmallQuestionData(this.reportParam)
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectSmallQuestionData(this.reportParam).then(res => {
           if (res.code == 200 && res.data) {
             const tableData = res.data.tableData || []
             const titleData = res.data.titleData || []
@@ -643,6 +711,21 @@ export default {
           this.groupData.smallQuestionData.loading = false
         })
     },
+    //知识点分层分析表(联考)
+    QueryStudentKnowLedgeLayering(){
+      this.$api.reportStudent[getApiName()].queryStudentKnowLedgeLayering(this.reportParam).then(res => {
+        if (res.code == 200 && res.data) {
+          const { breakThrough,develop,fullScore } = res.data;
+          this.knowLedgeLayering.breakThrough = breakThrough || '';
+          this.knowLedgeLayering.develop = develop || '';
+          this.knowLedgeLayering.fullScore = fullScore || '';
+        } else {
+          this.knowLedgeLayering.breakThrough = '';
+          this.knowLedgeLayering.develop = '';
+          this.knowLedgeLayering.fullScore = '';
+        }
+      })
+    },
     GetDifficultyClass(val) {
       if (val == 1) {
         return 'difficulty easy'
@@ -703,9 +786,7 @@ export default {
       this.groupData.bigQuestionData.loading = true
       this.groupData.knowledgePointQuestionData.loading = true
       this.groupData.abilityQuestionData.loading = true
-      this.$api.reportStudent
-        .queryOneSubjectGroupQuestionData(this.reportParam)
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectGroupQuestionData(this.reportParam).then(res => {
           if (res.code == 200 && res.data) {
             const { bigQuestion, knowledgePointQuestion, abilityQuestion } = res.data
             if (bigQuestion && bigQuestion?.tableData?.length > 0) {
@@ -768,7 +849,7 @@ export default {
     },
     //学生端查询单科-自定义分组
     QueryOneSubjectCustomGroupQuestion() {
-      this.$api.reportStudent.queryOneSubjectCustomGroupQuestion(this.reportParam).then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectCustomGroupQuestion(this.reportParam).then(res => {
         if (res.code == 200 && res.data) {
           const customQuestionData = res?.data?.customQuestionData ?? []
           if (customQuestionData && customQuestionData.length > 0) {
@@ -802,9 +883,7 @@ export default {
     //学生端查询单科-历次查询
     QueryOneSubjectHistoryExamData() {
       this.historyExamLoading = true
-      this.$api.reportStudent
-        .queryOneSubjectHistoryExamData(this.reportParam)
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectHistoryExamData(this.reportParam).then(res => {
           if (res.code == 200 && res.data) {
             const detailData = (res.data?.detailData || []).reverse()
             this.historyExamData.chartData = detailData
@@ -852,7 +931,7 @@ export default {
     },
     //学生端查询单科-总结建议
     QueryOneSubjectSuggestionData() {
-      this.$api.reportStudent.queryOneSubjectSuggestionData(this.reportParam).then(res => {
+      this.$api.reportStudent[getApiName()].queryOneSubjectSuggestionData(this.reportParam).then(res => {
         if (res.code == 200 && res.data) {
           const data = res.data
           //* 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
@@ -1049,4 +1128,84 @@ export default {
     background: #ee6666;
   }
 }
+.module_table{
+ &.pb20{
+  padding-bottom: 20px;
+ }
+}
+.student_know_paper{
+  width: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  border-radius: 6px;
+  border: 1px solid #EBEEF5;
+  li{
+      width: 100%;
+      border-top: 1px solid #EBEEF5;
+      box-sizing: border-box;
+      display: inline-flex;
+      &:first-child{
+          border-top: 0;
+          .li_left{
+              background-color: #EEF1FB;
+              span{
+                  color: #5470C6;
+              }
+          }
+      }
+      &:nth-child(2){
+          .li_left{
+              background-color: #ECF6F1;
+              span{
+                  color: #3BA272;
+              }
+          }
+      }
+      &:nth-child(3){
+          .li_left{
+              background-color: #FFFAEF;
+              span{
+                  color: #FAC858;
+              }
+          }
+      }
+      .li_left{
+          width: 300px;
+          padding: 24px;
+          border-right: 1px solid #EBEEF5;
+          box-sizing: border-box;
+          display: inline-flex;
+          flex-direction: column;
+          justify-content: center;
+          flex-shrink: 0;
+          span{
+              font-weight: 500;
+              font-size: 20px;
+              line-height: 28px;
+              display: block;
+              text-align: center;
+              &:nth-child(2){
+                margin-top: 4px;
+                line-height: 20px;
+                font-weight: 400;
+                font-size: 14px;
+                color: #999999;
+              }     
+          }
+      }
+      .li_know{
+          flex: 1;
+          padding: 20px;
+          box-sizing: border-box;
+          font-weight: 400;
+          font-size: 14px;
+          color: #666666;
+          line-height: 24px;
+          text-align: justified;
+          display: inline-flex;
+          align-items: center;
+      }
+  }
+}
 </style>

+ 61 - 23
src/views/analysisReport/studentPage/scrolReport/transcript_total.vue

@@ -34,13 +34,17 @@
                 * 3-得分显示对错,小题分显示分数,4-得分显示对错,小题分显示对错
                 * 5-得分显示等级,小题分显示分数,6-得分显示等级,小题分显示对错 -->
                 <template v-if="subjectData.studentOpenness == 3 || subjectData.studentOpenness == 4">
-                  <img v-if="scope.row.fullScore == scope.row?.[title.prop]" src="@/assets/stuIcon/icon_right.png" />
-                  <img v-else-if="scope.row?.[title.prop] == 0" src="@/assets/stuIcon/icon_error.png" />
-                  <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                  <template v-if="!isNaN(scope.row?.[title.prop])">
+                    <img v-if="scope.row.fullScore == scope.row?.[title.prop]" src="@/assets/stuIcon/icon_right.png" />
+                    <img v-else-if="scope.row?.[title.prop] == 0" src="@/assets/stuIcon/icon_error.png" />
+                    <img v-else src="@/assets/stuIcon/icon_half_right.png" />
+                  </template>
+                  <template v-else>{{ scope.row?.[title.prop] ?? '-' }}</template>
                 </template>
-                <template v-else>{{ scope.row?.[title.prop] || '-' }}</template>
+                <template v-else>{{ scope.row?.[title.prop] ?? '-' }}</template>
               </template>
-              <template v-else>{{ scope.row?.[title.prop] || '-' }}</template>
+              <template v-else-if="title.prop == 'groupClassRanks' || title.prop == 'groupClassMaxScoreList' || title.prop == 'groupClassAvgScoreList'">{{ GetGroupClassValue(scope.row?.[title.prop],title.prop,title.code)}}</template>
+              <template v-else>{{ scope.row?.[title.prop] ?? '-' }}</template>
             </template>
           </el-table-column>
           <!-- 查看答题卡列固定在右侧 -->
@@ -63,10 +67,14 @@
             </template>
           </el-table-column>
         </el-table>
-        <div class="page_jg_20"></div>
+        <div class="module_describe">
+          <ExpandableText>
+            说明:为了更好地反馈学习情况,将采用等级形式呈现学业结果。以下是学业等级划分的具体计算方式,请知悉。本次考试等级按照<span style="color: #2E64FA;">{{ this.academicLevelData.scoreType==1?'分数':'排名' }}占比区间</span>划分,<template v-if="academicLevelData.examAcademicLevelStr!=null">共分为{{ academicLevelData.num }}个等级:{{ academicLevelData.examAcademicLevelStr }}</template>等级是动态的,它只描述过去,却指引未来。你的每一次努力,都在为下一个等级重新定义!
+          </ExpandableText>
+        </div>
       </div>
     </div>
-    <div class="report_module" v-if="suggestionData?.fullScore!='-'">
+    <div class="report_module" v-if="suggestionData?.fullScore!='-' && suggestionData!=null">
       <div class="module_title">
         <div class="title_left">总结建议</div>
       </div>
@@ -115,17 +123,12 @@
       </div>
       <div class="module_describe">
         <ExpandableText>
-          说明:从标准分情况来看,这次考试<template v-if="subjectData.maxSubject"
-            ><span style="color: #3ba272">{{ subjectData.maxSubject }}</span
-            >表现突出,请继续保持</template
-          ><template v-if="subjectData.minSubject"
-            >;<span style="color: #ee6666">{{ subjectData.minSubject }}</span
-            >标准分明显低于其他学科,可能会对总体排名造成影响,可结合错题梳理核心知识点,精准定位薄弱环节,制定针对性的提升计划,以实现各科均衡发展,进一步巩固整体成绩</template
-          >。
+          说明:标准分,也称为标准分数或Z分数,是一种由原始分数推导出来的相对地位量数,用于说明原始分数在所属分数群体中的相对位置。其计算公式为:Z = (x - μ)/σ(Z是标准分,X是原始数据的值,μ是该组数据的平均数,σ是该组数据的标准差)。标准分的分数区间通常在-3到+3之间,标准分大于0,表示成绩在平均数之上;标准分数小于0,表示成绩在平均分数之下。
+          从标准分情况来看,这次考试<template v-if="subjectData.maxSubject"><span style="color: #3ba272">{{ subjectData.maxSubject }}</span>表现突出,请继续保持</template><template v-if="subjectData.minSubject">;<span style="color: #ee6666">{{ subjectData.minSubject }}</span>标准分明显低于其他学科,可能会对总体排名造成影响,可结合错题梳理核心知识点,精准定位薄弱环节,制定针对性的提升计划,以实现各科均衡发展,进一步巩固整体成绩</template>。
         </ExpandableText>
       </div>
     </div>
-    <div class="report_module" v-if="false">
+    <div class="report_module" v-if="showHistory==1">
       <div class="module_title">
         <div class="title_left">历次考试分析</div>
         <div class="title_right report_button">
@@ -188,6 +191,7 @@ import ExpandableText from '@/views/analysisReport/components/ExpandableText' //
 import LineChart from '@/views/analysisReport/components/dCharts/lineChart' //折线图
 import GotoTop from '@/views/analysisReport/components/GotoTop' //分析报告页面底部回到顶部组件
 import StudentPaper from '@/components/StudentPaper.vue' //学生答题卡预览组件
+import {getApiName} from '@/utils/common';
 import { mapGetters } from 'vuex'
 export default {
   name: 'subjectQuality',
@@ -201,6 +205,11 @@ export default {
   },
   data() {
     return {
+      academicLevelData:{
+        examAcademicLevelStr:'',
+        scoreType:'',
+        num:''
+      },
       subjectData: {
         standardScoreAnalysisStatus: 0,
         titleData: [],
@@ -229,7 +238,8 @@ export default {
       showStudentPaperDialog: false, //显示答题卡弹框
       paperInfo: {},
       paperTitle: '', //答题卡弹框标题
-      currentPageIndex: 0 //当前选中的答题卡第几页
+      currentPageIndex: 0, //当前选中的答题卡第几页
+      showHistory:0
     }
   },
   watch: {
@@ -277,17 +287,47 @@ export default {
   },
   methods: {
     PageInit() {
+      this.QueryOneSubjectData() //学生端查询单科-我的成绩
       this.QueryMultiSubjectData() //多科成绩总览-科目标准分分析
       this.QueryHistoryExamData() //学生端查询总分,多科历次信息
       this.QuerySuggestionData() //学生端查询总分,多科总结建议信息
     },
+    GetGroupClassValue(data,prop,code){
+      const obj = data.find(item=>item.classGroupCode==code);
+      const val = prop=='groupClassRanks'?obj?.classGroupRankRate:obj?.groupClassResultScore;
+      return val ?? '-'
+    },
+    //学生端查询单科-我的成绩
+    QueryOneSubjectData() {
+      this.subjectLoading = true
+      this.$api.reportStudent[getApiName()].queryOneSubjectData(this.reportParam).then(res => {
+          if (res.code == 200 && res.data) {
+            const levelList = res.data?.academicLevelData?.examAcademicLevelList || [];
+            const scoreType = res.data?.academicLevelData?.scoreType;
+            this.academicLevelData.examAcademicLevelStr = '';
+            const lens = levelList.length;
+            this.academicLevelData.num = lens;
+            levelList.forEach((item,index)=>{
+              const gradeName = item.gradeName || '';
+              const level = gradeName.replace(/等级/g, '');
+              const startScore = String(item.startScore).replace(/\.0*$/, '');
+              const endScore = String(item.endScore).replace(/\.0*$/, '');
+              this.academicLevelData.examAcademicLevelStr +=`${level}${index==0?'[':'('}${startScore}%-${endScore}%]${index==lens -1?'。':';'}`;
+            })
+            this.academicLevelData.scoreType = scoreType;
+            this.showHistory = res?.data?.showHistory || 0;
+          } else {
+            this.academicLevelData.examAcademicLevelStr = '';
+            this.academicLevelData.scoreType = '';
+            this.academicLevelData.num = '';
+          }
+        })
+    },
     //多科成绩总览-科目标准分分析
     QueryMultiSubjectData() {
       //  获取表格数据
       this.subjectLoading = true
-      this.$api.reportStudent
-        .queryMultiSubjectData(this.reportParam)
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryMultiSubjectData(this.reportParam).then(res => {
           if (res.code == 200 && res.data) {
             this.subjectData.tableData = res.data.tableData || []
             this.subjectData.studentOpenness = res.data.studentOpenness ?? ''
@@ -363,9 +403,7 @@ export default {
         //学生端查询总分,多科历次信息,组合科目时,一定要传组合科目的名称,组合科目的历次是按照组合科目名称来查询
         param.subjectName = this.getSubjectName
       }
-      this.$api.reportStudent
-        .queryHistoryExamData({ ...param })
-        .then(res => {
+      this.$api.reportStudent[getApiName()].queryHistoryExamData({ ...param }).then(res => {
           if (res.code == 200 && res.data) {
             const detailData = (res.data?.detailData || []).reverse()
             this.historyExamData.chartData = detailData
@@ -413,7 +451,7 @@ export default {
     },
     //学生端查询总分,多科总结建议信息
     QuerySuggestionData() {
-      this.$api.reportStudent.querySuggestionData(this.reportParam).then(res => {
+      this.$api.reportStudent[getApiName()].querySuggestionData(this.reportParam).then(res => {
         if (res.code == 200 && res.data) {
           const data = res.data
           this.suggestionData = data

+ 0 - 0
src/views/analysisReport/wrongQuestion/AllQuestion.vue


+ 0 - 0
src/views/analysisReport/wrongQuestion/HasQuestion.vue


+ 1 - 1
src/views/analysisReport/wrongQuestion/KnowledgePoint.vue

@@ -6,7 +6,7 @@
 export default {
     props: {
         text: String
-    }
+    },
 }
 </script>
 

+ 0 - 0
src/views/analysisReport/wrongQuestion/NoQuestion.vue


+ 11 - 0
src/views/analysisReport/wrongQuestion/Notbook.vue

@@ -0,0 +1,11 @@
+<template>
+
+</template>
+
+<script>
+export default {
+
+}
+</script>
+
+<style></style>

+ 256 - 0
src/views/analysisReport/wrongQuestion/Preview.vue

@@ -0,0 +1,256 @@
+<template>
+    <div>
+        <template v-if="isTextMode">
+            <template v-if="isFromXK">
+                <div class="text-preview" style="color: #000 !important;" ref="previewRoot" v-html="displayHtml"></div>
+            </template>
+            <template v-else>
+                <div style="color: #000 !important;" v-html="displayHtml"></div>
+            </template>
+        </template>
+
+        <template v-if="isImageMode">
+            <el-image v-if="hasImage && !imageError" class="question_img" :src="imgSrc" :preview-src-list="[imgSrc]"
+                @load="onImageLoad" @error="onImageError">
+            </el-image>
+
+            <div v-else-if="imageLoading && !imageError" class="image-loading">正在加载图片...</div>
+
+            <div v-else-if="imageError" class="image-error">图片加载失败</div>
+
+            <div v-else class="image-empty">暂无图片</div>
+        </template>
+    </div>
+</template>
+
+<script>
+const FORMULA_SELECTOR = 'span[data-w-e-type="formula"][data-value]';
+
+function isWangEditorFormula(html = "") {
+    return html && html.indexOf('data-w-e-type="formula"') > -1;
+}
+
+export default {
+    name: "Preview",
+
+    props: {
+        content: {
+            type: String,
+            default: "",
+        },
+        type: [String, Number],
+        imgBase64: {
+            type: [String, Array],
+            default: ''
+        }
+    },
+
+    data() {
+        return {
+            displayHtml: "",
+            isFromXK: false,
+            imageLoading: false,
+            imageError: false,
+        };
+    },
+
+    computed: {
+        isTextMode() {
+            return String(this.type) === "1";
+        },
+        isImageMode() {
+            return String(this.type) === "2";
+        },
+        imgSrc() {
+            return this.normalizeImg(this.imgBase64);
+        },
+        hasImage() {
+            return this.imgSrc !== '';
+        },
+    },
+
+    watch: {
+        content: {
+            immediate: true,
+            handler() {
+                this.updateDisplay();
+            },
+        },
+
+        type() {
+            this.updateDisplay();
+        },
+
+        imgBase64: {
+            immediate: true,
+            handler() {
+                const src = this.normalizeImg(this.imgBase64);
+                this.imageLoading = !!src;
+                this.imageError = false;
+            }
+        }
+    },
+
+    mounted() {
+        // mounted 保持空,渲染由 updateDisplay 控制
+    },
+
+    methods: {
+        async updateDisplay() {
+            if (!this.isTextMode) {
+                this.displayHtml = "";
+                return;
+            }
+
+            const value = this.content || "";
+            if (!value) {
+                this.displayHtml = "";
+                return;
+            }
+
+            if (!isWangEditorFormula(value)) {
+                this.isFromXK = false;
+                this.displayHtml = value;
+                return;
+            }
+
+            const container = document.createElement('div');
+            container.innerHTML = value;
+            const spans = Array.from(container.querySelectorAll(FORMULA_SELECTOR));
+
+            if (spans.length === 0) {
+                this.isFromXK = false;
+                this.displayHtml = value;
+                return;
+            }
+
+            if (window.MathJax) {
+                if (window.MathJax.tex2svgPromise) {
+                    for (const span of spans) {
+                        const latex = span.getAttribute('data-value') || '';
+                        try {
+                            const node = await window.MathJax.tex2svgPromise(latex, { display: false });
+                            span.replaceWith(node);
+                        } catch (e) {
+                            console.warn('tex2svgPromise 渲染失败,保留原始公式标记', e);
+                            span.textContent = `\\(${span.getAttribute('data-value')}\\)`;
+                        }
+                    }
+                    this.displayHtml = container.innerHTML;
+                } else {
+                    for (const span of spans) {
+                        const latex = span.getAttribute('data-value') || '';
+                        const textNode = document.createTextNode(`\\(${latex}\\)`);
+                        span.replaceWith(textNode);
+                    }
+                    this.isFromXK = true;
+                    this.displayHtml = container.innerHTML;
+
+                    this.$nextTick(async () => {
+                        try {
+                            if (window.MathJax.typesetPromise) {
+                                await window.MathJax.typesetPromise([this.$refs.previewRoot]);
+                            }
+                        } catch (error) {
+                            console.error('MathJax typeset 失败', error);
+                        }
+                    });
+                }
+            } else {
+                this.displayHtml = value;
+            }
+
+            this.$nextTick(() => {
+                this.$emit('rendered');
+            });
+        },
+
+        normalizeImg(src) {
+            if (Array.isArray(src)) return src[0] || '';
+            if (typeof src === 'string') return src.trim();
+            return '';
+        },
+
+        onImageLoad() {
+            this.imageLoading = false;
+            this.imageError = false;
+        },
+
+        onImageError() {
+            this.imageLoading = false;
+            this.imageError = true;
+        }
+    }
+};
+</script>
+
+<style scoped>
+/* MathJax CHTML 覆盖,缓解全局样式对公式的影响 */
+.mjx-chtml {
+    display: inline-block;
+    line-height: 1;
+    vertical-align: baseline;
+}
+
+.mjx-container,
+.mjx-char,
+.mjx-mfrac {
+    line-height: 1 !important;
+    display: inline-block !important;
+    vertical-align: baseline !important;
+}
+
+.mjx-msqrt>.mjx-surd {
+    vertical-align: -0.12em !important;
+}
+
+.el-image {
+    max-width: 720px;
+}
+</style>
+
+<style lang="scss">
+.text-preview {
+    img {
+        vertical-align: middle !important;
+    }
+
+    table {
+        width: 100%;
+        border-collapse: collapse;
+        border: 1px solid #ebeef5;
+        text-align: center;
+    }
+
+    table td,
+    table th {
+        padding: 0 10px;
+        border: 1px solid #ebeef5;
+        color: #666;
+        height: 30px;
+    }
+
+    // 第一行作为默认头部
+    // table tbody tr:first-child {
+    //     background-color: #F5F7FA;
+    //     font-weight: bold;
+    // }
+
+    table thead th {
+        background-color: #F5F7FA;
+        width: 100px;
+    }
+
+    table tr:nth-child(odd) {
+        background: #F9F9F9;
+    }
+
+    table tr:nth-child(even) {
+        background: #fff;
+    }
+}
+
+.question_img {
+    max-width: 720px;
+}
+</style>

+ 193 - 66
src/views/analysisReport/wrongQuestion/index.vue

@@ -11,13 +11,14 @@
 
         <div class="content">
             <div class="right_btn" v-if="errorType !== 1">
-                <el-button size="medium" @click="downloadDialogVisible = true; isVariation = 0">下载错题本</el-button>
-                <el-button type="primary" size="medium"
-                    @click="downloadDialogVisible = true; isVariation = 1">下载个性化提升手册</el-button>
+                <el-button size="medium" :disabled="!canBtnClick"
+                    @click="downloadDialogVisible = true; isVariation = 0">下载错题本</el-button>
+                <el-button :disabled="!canBtnClick" type="primary" size="medium"
+                    @click="downLoadPapers">下载个性化提升手册</el-button>
             </div>
 
             <Download :visible.sync="downloadDialogVisible" :examId="examId" :subjectCode="subjectCode"
-                :isVariation="isVariation" />
+                :isVariation="isVariation" :questionList="questionList" />
 
             <template v-if="questionList.length > 0">
                 <div v-for="(question, index) in questionList" :key="question.questionId">
@@ -26,17 +27,22 @@
                             <div class="card_top_info">
                                 <div class="left">
                                     <span class="number mr_10">{{ countGlobalIndex(index + 1) }}</span>
-                                    <span class="gray">试题类型:</span>
-                                    <span class="black mr_10">{{ question.questionType }}</span>
+                                    <span class="gray">试题类型:<span v-if="!question.questionType">暂无试题类型</span></span>
+                                    <span class="black mr_10" style="font-weight: 500;">
+                                        {{ question.questionType }}
+                                    </span>
                                     <span class="tag" v-if="question.classScoreRate < 40">高频错题</span>
                                 </div>
                             </div>
 
-                            <div class="question_content line_height_20" v-if="question.sourceType === 1"
-                                v-html="question.questionData.questionStem"></div>
-
-                            <div class="question_content" v-if="question.sourceType === 2">
-                                <img :src="question.questionImg" alt="" width="100%">
+                            <div class="question_content line_height_20">
+                                <template v-if="question.questionData?.questionStem || question.questionImg">
+                                    <Preview :type="question.sourceType" :content="question.questionData?.questionStem"
+                                        :imgBase64="question.questionImg" />
+                                </template>
+                                <template v-else>
+                                    <div style="color: #999;">暂无数据</div>
+                                </template>
                             </div>
 
                             <div class="card_footer">
@@ -91,14 +97,16 @@
                                 </template>
 
                                 <template v-if="errorType === 1 || question.errorStatus === 1">
-                                    <span class="black ml_30" size="small" @click="_markStudentErrorQuestion(question, 0)">
+                                    <span class="black ml_30" size="small"
+                                        @click="_markStudentErrorQuestion(question, 0)">
                                         <i class="iconfont icon_biaojiweizhangwo"></i>
                                         标记为未掌握
                                     </span>
                                 </template>
 
                                 <template v-if="errorType === 2 || question.errorStatus === 2">
-                                    <span class="black ml_30" size="small" @click="_markStudentErrorQuestion(question, 0)">
+                                    <span class="black ml_30" size="small"
+                                        @click="_markStudentErrorQuestion(question, 0)">
                                         <i class="iconfont icon_yichufuxiben"></i>
                                         移除复习本
                                     </span>
@@ -107,45 +115,74 @@
 
                             <div class="content" v-if="question.parseShow && !question.answerShow">
                                 <div class="flex">
-                                    <div class="flex_left" style="line-height: 28px;">知识点:</div>
-                                    <div class="flex_right">
-                                        <template v-if="question.knowledgePoint && question.knowledgePoint.length > 0">
-                                            <KnowledgePoint v-for="(know, index) in question.knowledgePoint"
-                                                :key="index" :text="know" />
-                                        </template>
-                                    </div>
+                                    <template v-if="question.knowledgePoint && question.knowledgePoint.length > 0">
+                                        <div class="flex_left" style="line-height: 28px;">【知识点】</div>
+                                        <div class="flex_right">
+                                            <KnowledgePoint v-for="know in question.knowledgePoint" :key="know"
+                                                :text="know" />
+                                        </div>
+                                    </template>
+
+                                    <template v-else>
+                                        <div class="flex_left">【知识点】</div>
+                                        <div class="flex_right">
+                                            <div style="color: #999;">暂无数据</div>
+                                        </div>
+                                    </template>
                                 </div>
 
                                 <div class="flex">
-                                    <div class="flex_left">答&nbsp;&nbsp;&nbsp;&nbsp;案:</div>
+                                    <div class="flex_left">【答&nbsp;&nbsp;&nbsp;&nbsp;案】</div>
                                     <div class="flex_right">
-                                        <div v-if="question.sourceType === 1" v-html="question.questionData.answer">
-                                        </div>
-                                        <div v-if="question.sourceType === 2">
-                                            <img :src="question.answerImg" alt="" width="100%" />
-                                        </div>
+                                        <template
+                                            v-if="question.questionData?.answer || question.answerImg || question.answer">
+                                            <template v-if="question.isObjective === 1">
+                                                <div>{{ question.answer }}</div>
+                                            </template>
+
+                                            <template v-else>
+                                                <Preview :type="question.sourceType"
+                                                    :content="question.questionData?.answer"
+                                                    :imgBase64="question.answerImg" />
+                                            </template>
+                                        </template>
+
+                                        <template v-else>
+                                            <div style="color: #999;">暂无数据</div>
+                                        </template>
                                     </div>
                                 </div>
 
                                 <div class="flex">
-                                    <div class="flex_left">解&nbsp;&nbsp;&nbsp;&nbsp;析:</div>
+                                    <div class="flex_left">【解&nbsp;&nbsp;&nbsp;&nbsp;析】</div>
                                     <div class="flex_right">
-                                        <div style="line-height: 1.5;" v-show="question.sourceType === 1"
-                                            v-html="question.questionData.analysis">
-                                        </div>
-                                        <div v-if="question.sourceType === 2">
-                                            <img :src="question.parseImg" alt="" width="100%" />
-                                        </div>
+                                        <template v-if="question.questionData?.analysis || question.parseImg">
+                                            <Preview :type="question.sourceType"
+                                                :content="question.questionData?.analysis"
+                                                :imgBase64="question.parseImg" />
+                                        </template>
+                                        <template v-else>
+                                            <div style="color: #999;">暂无数据</div>
+                                        </template>
                                     </div>
                                 </div>
                             </div>
 
                             <div class="content" v-if="question.answerShow && !question.parseShow">
-                                <div class="content_inner" v-if="question.sourceType === 1"
-                                    v-html="question.studentAnswer"></div>
-                                <div class="content_inner" v-if="question.sourceType === 2">
-                                    <img :src="question.studentAnswerImg" alt="" width="100%" />
-                                </div>
+                                <template v-if="question.isObjective === 1">
+                                    <div v-if="question.studentAnswer">{{ question.studentAnswer }}</div>
+                                    <div v-else style="color: #999;">暂无数据</div>
+                                </template>
+
+                                <template v-else>
+                                    <template v-if="picUrl && questionVOS">
+                                        <div style="height: 300px;">
+                                            <PaperImage :imageIndex="index" :scoreFontSize="20" :rtOrWrSize="10"
+                                                :paperImgUrl="picUrl" :drawData="questionVOS" />
+                                        </div>
+                                    </template>
+                                    <div v-else style="color: #999;">暂无数据</div>
+                                </template>
                             </div>
                         </div>
                     </div>
@@ -171,19 +208,21 @@
                                 </div>
                             </div>
 
-                            <div class="question_content line_height_20"
-                                v-html="question.variationQuestion.questionStem">这是题目内容</div>
+                            <div class="question_content line_height_20">
+                                <Preview :type="1" :content="question.variationQuestion?.questionStem" />
+                            </div>
 
                             <!-- <div class="yuwen">
                                 <span class="btn_span active">1</span>
                                 <span class="btn_span">2</span>
-
                                 <div class="question_list"></div>
                             </div> -->
 
                             <div class="card_footer">
                                 <div class="gray">知识点:</div>
-                                <KnowledgePoint :text="question.variationQuestion.knowledgePoint" />
+                                <template v-for="(know, index) in question.variationQuestion.knowledgePoint">
+                                    <KnowledgePoint :text="know" />
+                                </template>
                             </div>
                         </div>
                     </div>
@@ -207,17 +246,25 @@ import {
     queryStudentErrorQuestion,
     markStudentErrorQuestion,
     downloadStudentErrorQuestion,
+    findStudentCard
 } from '../../../http/api/errorQuestion';
 import Download from './Download.vue';
-import loadImg from './loadImg.js'
+import { mergeImage } from './loadImg.js'
 import { Loading } from 'element-ui';
 import NoData from './NoData/NoData.vue';
 import KnowledgePoint from './KnowledgePoint.vue';
+import Preview from './Preview.vue';
+import PaperImage from '@/components/PaperImage.vue';
+import { confirm } from '../../../utils/common.js'
+
 export default {
     components: {
         Download,
         NoData,
         KnowledgePoint,
+        Preview,
+        PaperImage,
+
     },
 
     data() {
@@ -242,6 +289,9 @@ export default {
 
             downloadDialogVisible: false,
             isVariation: 0,
+            picUrl: null,
+            questionVOS: [],
+
         };
     },
 
@@ -253,6 +303,22 @@ export default {
         IsVariation() {
             return this.$store.state.question.isVariation;
         },
+
+        canBtnClick() {
+            const has = this.questionList.find(question => {
+                return !question.questionImg && !question.questionData?.questionStem
+            })
+
+            console.log('has', has)
+
+            if (has) {
+                this.$store.commit('question/setCanDownloanBtnClick', false)
+                return false
+            } else {
+                this.$store.commit('question/setCanDownloanBtnClick', true)
+                return true
+            }
+        }
     },
 
     watch: {
@@ -301,7 +367,6 @@ export default {
 
         countLevel(num) {
             // 1 :拔高题  2:进阶题  3:巩固题
-            console.log('num', num);
             const arr = ['拔高题', '进阶题', '巩固题']
             const color = ['bg_hard', 'bg_medium', 'bg_easy',]
             return {
@@ -312,8 +377,39 @@ export default {
 
         ShowAnserAndParse(type, question) {
             if (type === 1) {
-                question.answerShow = !question.answerShow;
-                question.parseShow = false
+                if (question.isObjective === 1) {
+                    question.answerShow = !question.answerShow;
+                    question.parseShow = false
+                    return;
+                }
+
+                findStudentCard({
+                    examId: this.examId,
+                    subjectCode: this.subjectCode,
+                    questionId: question.questionId,
+                }).then(res => {
+                    if (res.code === 200) {
+                        if (res.data && res.data.pageVOS && res.data.pageVOS.length > 0) {
+
+
+                            const data = res.data.pageVOS[0]
+                            this.picUrl = data.picUrl;
+                            this.questionVOS = data.questionVOS.map(item => {
+                                return {
+                                    ...item,
+                                    samplingPosition: '{"x":195,"y":247,"page":1}'
+                                }
+                            });
+                        } else {
+                            this.picUrl = null;
+                            this.questionVOS = [];
+                        }
+
+
+                        question.answerShow = !question.answerShow;
+                        question.parseShow = false
+                    }
+                })
             }
 
             if (type === 2) {
@@ -338,27 +434,34 @@ export default {
                 this.uploadPaperUrls = uploadPaperUrls;
                 this.pageParam.total = res.data.total * 1;
 
-                for (let i = 0; i < questionList.length; i++) {
-                    const { sourceType, titleCoordinates, answerCoordinates, parseCoordinates, paintingPosition } = questionList[i]
-                    questionList[i].answerShow = false;
-                    questionList[i].parseShow = false;
-                    if (sourceType === 2) {
-                        const questionImg = await loadImg(this.uploadPaperUrls, titleCoordinates);
-                        questionList[i].questionImg = questionImg;
-
-                        const answerImg = await loadImg(this.answerUrls, answerCoordinates);
-                        questionList[i].answerImg = answerImg;
-
-                        const parseImg = await loadImg(this.answerUrls, parseCoordinates);
-                        questionList[i].parseImg = parseImg;
+                // 并行合并图片:先为每道题准备 promise,然后使用 Promise.all 并发执行
+                const processedQuestions = await Promise.all(questionList.map(async (q) => {
+                    q.answerShow = false;
+                    q.parseShow = false;
+                    const { sourceType, titleCoordinates, answerCoordinates, parseCoordinates } = q;
 
-                        // TODO: 学生答案图片加载,由于
-                        const studentAnswerImg = await loadImg(this.answerUrls, paintingPosition);
-                        questionList[i].studentAnswerImg = studentAnswerImg;
+                    if (sourceType === 2) {
+                        try {
+                            const [questionImg, answerImg, parseImg] = await Promise.all([
+                                mergeImage(this.uploadPaperUrls, titleCoordinates),
+                                mergeImage(this.answerUrls, answerCoordinates),
+                                mergeImage(this.answerUrls, parseCoordinates),
+                            ]);
+                            q.questionImg = questionImg || '';
+                            q.answerImg = answerImg || '';
+                            q.parseImg = parseImg || '';
+                        } catch (e) {
+                            // 如果合并过程中某张图失败,记录空字符串并继续,避免阻塞全部渲染
+                            q.questionImg = q.questionImg || '';
+                            q.answerImg = q.answerImg || '';
+                            q.parseImg = q.parseImg || '';
+                        }
                     }
-                }
 
-                this.questionList = questionList;
+                    return q;
+                }));
+
+                this.questionList = processedQuestions;
 
                 // 缓慢滚动到顶部
                 this.scrollToTop();
@@ -398,6 +501,30 @@ export default {
             this.pageParam.pageNum = page;
             await this._queryStudentErrorQuestion();
         },
+
+        hasTuozhan() {
+            // 查看题目中第一个 变式题为空 的题目
+            const questionWithVariation = this.questionList.find(q => !q.variationQuestion);
+
+            if (questionWithVariation) {
+                return false; // 存在题目没有变式题
+            }
+
+            return true; // 所有题目都有变式题
+        },
+
+        downLoadPapers() {
+            const hasVariation = this.hasTuozhan();
+            if (!hasVariation) {
+                confirm('教师还未确认变式题,是否继续下载?', () => {
+                    this.downloadDialogVisible = true;
+                    this.isVariation = 1;
+                })
+            } else {
+                this.downloadDialogVisible = true;
+                this.isVariation = 1;
+            }
+        }
     },
 }
 </script>
@@ -408,7 +535,7 @@ export default {
     margin-bottom: 10px;
 
     .flex_left {
-        width: 60px;
+        width: 80px;
     }
 
     .flex_right {
@@ -452,7 +579,7 @@ export default {
 }
 
 .question_card {
-    margin-bottom: 10px;
+    margin-bottom: 20px;
     border: 1px solid #ebeef5;
     border-radius: 10px;
     overflow: hidden;

+ 87 - 86
src/views/analysisReport/wrongQuestion/loadImg.js

@@ -1,106 +1,107 @@
-export default async function loadImg(urlList, CoordinateList) {
-    /**
-     * urlList: 图片地址数组
-     * 数据格式为 [url1, url2, url3]
-     * 
-     * CoordinateList: 坐标数组
-     * 数据格式为 [
-     *      { page: 1, x: 10, y: 20, w: 100, h: 150 },
-     *      { page: 2, x: 30, y: 40, w: 120, h: 160 },
-     * ]
-     * 也可能为
-     * [
-     *      { page: 2, x: 30, y: 40, w: 120, h: 160 },
-     *      { page: 4, x: 50, y: 60, w: 140, h: 180 },
-     * ]
-     * 
-     * 注意:page 为 1 时,对应 urlList 中的第一个图片地址
-     * 
-     * 功能描述:将多张图片根据坐标裁剪并合并为一张图片。
-     * 注意:
-     * (1)有可能裁剪区域的宽度不一致,合并后图片宽度取最大宽度,其他图片按比例缩放。
-     * (2)裁剪区域按顺序垂直排列,之间无间距。
-     * (3)page 可能不从 1 开始,且不连续。我们只需根据 page 值找到对应图片即可。
-     * 返回值:返回一个 Promise,resolve 合并后的图片的 Base64 数据 URL。
-     * 
-    */
-
+export async function mergeImage(urlList, CoordinateList) {
     if (!Array.isArray(urlList) || !Array.isArray(CoordinateList)) {
-        return null;
+        return '';
     }
 
     if (CoordinateList.length === 0 || urlList.length === 0) {
-        return null;
-    }
-
-    let maxWidth = 0;
-    for (let coord of CoordinateList) {
-        if (coord.w > maxWidth) {
-            maxWidth = coord.w;
-        }
+        return '';
     }
 
-    const imgList = []
+    const imageUrlList = []
 
-    for (let urlIndex = 0; urlIndex < urlList.length; urlIndex++) {
+    for (let i = 0; i < CoordinateList.length; i++) {
+        const { page, x, y, w, h } = CoordinateList[i];
+        const urlIndex = page - 1;
         const url = urlList[urlIndex];
-        for (let i = 0; i < CoordinateList.length; i++) {
-            const coord = CoordinateList[i];
-            const page = coord.page;
-            // 根据 maxWidth 计算缩放比例
-            const scale = maxWidth / coord.w;
-            
-            // 计算高度
-            coord.w = maxWidth;
-            coord.h = coord.h * scale;
-
-            if (page - 1 === urlIndex) {
-                const img = await loadImage(url);
-                const tempCanvas = document.createElement('canvas');
-                const tempCtx = tempCanvas.getContext('2d');
-                tempCanvas.width = coord.w;
-                tempCanvas.height = coord.h;
-
-                // 此处按 计算后的宽高进行裁剪
-                tempCtx.drawImage(img, coord.x, coord.y, coord.w, coord.h, 0, 0, coord.w, coord.h);
-                imgList.push({
-                    img: tempCanvas,
-                    width: coord.w,
-                    height: coord.h
-                });
-            }
-        }
-    }
-
-    // 创建最终合并的画布
-    const finalCanvas = document.createElement('canvas');
-    const finalCtx = finalCanvas.getContext('2d');
-    finalCanvas.width = maxWidth;
-    let totalHeight = 0;
-    for (let i = 0; i < imgList.length; i++) {
-        totalHeight += imgList[i].height;
+        const _url = GetImageUrl(url, x, y, w, h);
+        imageUrlList.push(_url);
     }
-    finalCanvas.height = totalHeight;
 
-    let currentY = 0;
-    for (let i = 0; i < imgList.length; i++) {
-        const img = imgList[i].img;
-        finalCtx.drawImage(img, 
-            0, 0, maxWidth, imgList[i].height, 
-            0, currentY, maxWidth, imgList[i].height
-        );
-        currentY += imgList[i].height;
+    if (imageUrlList.length === 1) {
+        return imageUrlList[0];
+    } else {
+        // 多张图片合并
+        const finalImage = await MergerImage(imageUrlList);
+        return finalImage;
     }
-
-    return finalCanvas.toDataURL('image/png');
 }
 
 function loadImage(url) {
     return new Promise((resolve, reject) => {
         const img = new Image();
         img.crossOrigin = "Anonymous";
-        img.src = url;
-        img.onload = () => resolve(img);
+        // 在 onload 中读取宽高,确保为真实尺寸
+        img.onload = () => {
+            const width = img.naturalWidth || img.width;
+            const height = img.naturalHeight || img.height;
+            resolve({
+                img,
+                width,
+                height
+            });
+        };
         img.onerror = (err) => reject(err);
+        img.src = url;
     })
+}
+
+export function GetImageUrl(base, x, y, w, h) {
+    return `${base}?x-oss-process=image/crop,x_${x},y_${y},w_${w},h_${h}`
+}
+
+export function MergerImage(imageUrlList) {
+    // 简化:只接受 url 或 base64 字符串数组
+    return new Promise(async (resolve, reject) => {
+        try {
+            // 确保是数组并过滤出有效字符串
+            const inputs = imageUrlList.filter(item => item);
+            if (inputs.length === 0) return resolve('');
+
+            // 使用 loadImage 加载并直接用返回的宽高创建 canvas(避免再去读取图片尺寸)
+            const canvases = [];
+            for (const url of inputs) {
+                const { img, width, height } = await loadImage(url);
+                const c = document.createElement('canvas');
+                c.width = width;
+                c.height = height;
+                const ctx = c.getContext('2d');
+                // 只绘制一次图片到 canvas 用于后续合并/缩放
+                ctx.drawImage(img, 0, 0, width, height);
+                canvases.push(c);
+            }
+
+            // 计算最大宽度并按比例缩放到该宽度
+            let maxWidth = 0;
+            for (const c of canvases) if (c.width > maxWidth) maxWidth = c.width;
+
+            const scaled = canvases.map(c => {
+                if (c.width === maxWidth) return c;
+                const scale = maxWidth / c.width;
+                const nc = document.createElement('canvas');
+                nc.width = maxWidth;
+                nc.height = Math.round(c.height * scale);
+                const ctx = nc.getContext('2d');
+                // 使用源 canvas 进行缩放绘制
+                ctx.drawImage(c, 0, 0, c.width, c.height, 0, 0, nc.width, nc.height);
+                return nc;
+            });
+
+            // 合并
+            let totalH = scaled.reduce((s, c) => s + c.height, 0);
+            const finalCanvas = document.createElement('canvas');
+            finalCanvas.width = maxWidth;
+            finalCanvas.height = totalH;
+            const fctx = finalCanvas.getContext('2d');
+
+            let y = 0;
+            for (const c of scaled) {
+                fctx.drawImage(c, 0, 0, c.width, c.height, 0, y, c.width, c.height);
+                y += c.height;
+            }
+
+            resolve(finalCanvas.toDataURL('image/jpeg'));
+        } catch (err) {
+            reject(err);
+        }
+    });
 }

+ 1 - 1
src/views/layout/components/AppMain.vue

@@ -1,6 +1,6 @@
 <template>
   <el-main class="appMain">
-    <router-view></router-view>
+    <router-view :key="$route.fullPath"></router-view>
   </el-main>
 </template>
 <script>

Неке датотеке нису приказане због велике количине промена