Sfoglia il codice sorgente

Merge branch 'develop5.0' into TPE/develop5.0

jeff 4 anni fa
parent
commit
724def33c2
45 ha cambiato i file con 3944 aggiunte e 122 eliminazioni
  1. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Snode.cs
  2. 0 3
      TEAMModelOS.SDK/Models/Cosmos/Common/Syllabus.cs
  3. 4 0
      TEAMModelOS/ClientApp/package.json
  4. 2 1
      TEAMModelOS/ClientApp/src/boot-app.js
  5. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/0.svg
  6. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/1.svg
  7. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/2.svg
  8. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/3.svg
  9. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/4.svg
  10. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/5.svg
  11. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/6.svg
  12. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/7.svg
  13. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/8.svg
  14. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/9.svg
  15. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/a.svg
  16. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/b.svg
  17. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/c.svg
  18. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/d.svg
  19. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/e.svg
  20. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/f.svg
  21. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/g.svg
  22. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/h.svg
  23. 7 0
      TEAMModelOS/ClientApp/src/icons/answersheet/i.svg
  24. 3 1
      TEAMModelOS/ClientApp/src/store/index.js
  25. 95 0
      TEAMModelOS/ClientApp/src/store/module/answerSheet.js
  26. 779 0
      TEAMModelOS/ClientApp/src/utils/dom_to_image.js
  27. 101 0
      TEAMModelOS/ClientApp/src/utils/html2pdf.js
  28. 371 0
      TEAMModelOS/ClientApp/src/utils/items.json
  29. 63 0
      TEAMModelOS/ClientApp/src/utils/sheetConfig.js
  30. 422 0
      TEAMModelOS/ClientApp/src/view/answersheet/BaseEditor.vue
  31. 103 0
      TEAMModelOS/ClientApp/src/view/answersheet/BaseObjective.vue
  32. 151 0
      TEAMModelOS/ClientApp/src/view/answersheet/BaseSvgBg.vue
  33. 330 0
      TEAMModelOS/ClientApp/src/view/answersheet/BaseTitleEditor.vue
  34. 262 0
      TEAMModelOS/ClientApp/src/view/answersheet/SheetBaseInfo.vue
  35. 106 0
      TEAMModelOS/ClientApp/src/view/answersheet/SheetComplete.vue
  36. 340 0
      TEAMModelOS/ClientApp/src/view/answersheet/SheetObjective.vue
  37. 119 0
      TEAMModelOS/ClientApp/src/view/answersheet/SheetSubjective.vue
  38. 134 0
      TEAMModelOS/ClientApp/src/view/answersheet/index.vue
  39. 7 0
      TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.less
  40. 33 4
      TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue
  41. 0 1
      TEAMModelOS/ClientApp/src/view/evaluation/components/BasePointPie.vue
  42. 1 1
      TEAMModelOS/ClientApp/src/view/evaluation/index/TestPaper.vue
  43. 225 46
      TEAMModelOS/ClientApp/src/view/learnactivity/ExamPaperAnalysis.vue
  44. 69 59
      TEAMModelOS/ClientApp/vue.config.js
  45. 90 5
      TEAMModelOS/Controllers/Syllabus/SyllabusController.cs

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Snode.cs

@@ -37,7 +37,7 @@ namespace TEAMModelOS.SDK.Models.Cosmos.Common
 
         public int order { get; set; }
         public List<string> points { get; set; } = new List<string> { "" };
-        public List<Rnode> rnodes { get; set; }
+        public List<Rnode> rnodes { get; set; } = new List<Rnode>();
         //public string code { get; set; }
 
     }

+ 0 - 3
TEAMModelOS.SDK/Models/Cosmos/Common/Syllabus.cs

@@ -18,9 +18,6 @@ namespace TEAMModelOS.SDK.Models
         /// 册别编码
         /// </summary>
         [Required(ErrorMessage = "{0} 必须填写")]
-      //  [PartitionKey]
-      //  public string volumeCode { get; set; }
-
         public List<Tnode> children { get; set; }
 
     }

+ 4 - 0
TEAMModelOS/ClientApp/package.json

@@ -33,6 +33,10 @@
 		"firebase": "^7.19.0",
 		"firestore": "^1.1.6",
 		"html2canvas": "^1.0.0-rc.7",
+		"imports-loader": "^0.8.0",
+		"jspdf": "^2.3.1",
+		"print-js": "^1.6.0",
+		"snapsvg": "^0.5.1",
 		"increase-memory-limit": "^1.0.7",
 		"js-sha1": "^0.6.0",
 		"json-markup": "^1.1.3",

+ 2 - 1
TEAMModelOS/ClientApp/src/boot-app.js

@@ -40,7 +40,8 @@ import konva from 'konva'
 import "@/assets/student-web/component_styles/common.css";
 import "@/assets/student-web/icons";
 import { User } from '@/service/User'
-
+import htmlToPdf from './utils/html2pdf'
+Vue.use(htmlToPdf);
 import { firestorePlugin } from "vuefire";
 Vue.use(firestorePlugin);
 

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/0.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">0</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/1.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">1</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/2.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">2</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/3.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">3</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/4.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">4</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/5.svg

@@ -0,0 +1,7 @@
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">5</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/6.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">6</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/7.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">7</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/8.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">8</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/9.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">9</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/a.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">A</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/b.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">B</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/c.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">C</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/d.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">D</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/e.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">E</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/f.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">F</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/g.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">G</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/h.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">H</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 7 - 0
TEAMModelOS/ClientApp/src/icons/answersheet/i.svg

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="abcd" version="1.1" viewBox="0 0 150 92" xml:space="preserve">
+	<!-- <rect x="0" y="0" width="150" height="92" style="fill-opacity: 0; stroke-width:13; stroke:black;" /> -->
+  <polyline points="38,0 0,0 0,92 38,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <line x1="0" y1="92" x2="38" y2="92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+  <text x="50%" y="83" font-size="112" text-anchor="middle">I</text>
+  <polyline points="112,0 150,0 150,92 112,92" style="fill-opacity: 0; stroke-width:13; stroke:black;"/>
+</svg>

+ 3 - 1
TEAMModelOS/ClientApp/src/store/index.js

@@ -14,6 +14,7 @@ import serviceDriveAuth from './module/serviceDriveAuth'
 import { GLOBAL } from '@/static/Global.js'
 import spaceAuth from './module/spaceAuth'
 import studentAclassOneAuth from './module/studentAclassOneAuth'
+import answerSheet from './module/answerSheet'
 
 
 Vue.use(Vuex)
@@ -104,6 +105,7 @@ export default new Vuex.Store({
         scboard,
         serviceDriveAuth,
         spaceAuth,
-        studentAclassOneAuth
+        studentAclassOneAuth,
+		answerSheet
     }
 })

+ 95 - 0
TEAMModelOS/ClientApp/src/store/module/answerSheet.js

@@ -0,0 +1,95 @@
+import {
+  PAPER_W,
+  PAPER_H,
+  SVG_BORDER_PROP,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML
+} from '../../utils/sheetConfig'
+export default {
+	state: {
+		count: 0,
+		pages: 1,
+		isNewPage: false,
+		needFixArr: [26],
+		editorInfos: {
+			subjectiveEditor1: {
+				height: 200,
+				content: '1111'
+			}
+		},
+		sheetConfig: {
+			// 答题卡宽高
+			pageConfig: {
+				width: PAPER_W,
+				height: PAPER_H
+			},
+			// 答题卡四个定位锚点
+			pageAnchor: [
+				[ANCHORPROP.gapX, ANCHORPROP.gapY, ANCHORPROP.width, ANCHORPROP.height],
+				[PAPER_W - ANCHORPROP.width - ANCHORPROP.gapX, ANCHORPROP.gapY, ANCHORPROP.width, ANCHORPROP
+				.height],
+				[ANCHORPROP.gapX, PAPER_H - ANCHORPROP.height - ANCHORPROP.gapY, ANCHORPROP.width, ANCHORPROP
+					.height],
+				[PAPER_W - ANCHORPROP.width - ANCHORPROP.gapX, PAPER_H - ANCHORPROP.height - ANCHORPROP.gapY,
+					ANCHORPROP.width, ANCHORPROP.height
+				]
+			],
+			// 答题卡外框
+			outerRect: [SVG_BORDER_PROP.x, SVG_BORDER_PROP.y, SVG_BORDER_PROP.width, SVG_BORDER_PROP.height],
+			// 基本信息框
+			baseInfoRect: [CONTENT_START_X, CONTENT_START_Y, INFO_W, INFO_H],
+			// 客观题框
+			objectiveRect: [],
+			// 填空题框
+			completeRect: [],
+			// 主观题框
+			subjectiveRect: []
+		}
+	},
+
+	mutations: {
+		addPage(state) {
+			state.pages++
+		},
+		clearPage(state) {
+			state.pages = 1
+		},
+		changeNewPage(state, val) {
+			state.isNewPage = val
+		},
+		addFixItem(state, val) {
+			state.needFixArr.push(val)
+		},
+		clearFixArr(state) {
+			state.needFixArr = []
+		},
+		setEditorHeight(state, val) {
+			if (state.editorInfos[val.id]) {
+				state.editorInfos[val.id].height = val.height
+			} else {
+				state.editorInfos[val.id] = {
+					height: val.height,
+					content: ''
+				}
+			}
+		}
+	},
+
+}

+ 779 - 0
TEAMModelOS/ClientApp/src/utils/dom_to_image.js

@@ -0,0 +1,779 @@
+(function (global) {
+    'use strict';
+
+    var util = newUtil();
+    var inliner = newInliner();
+    var fontFaces = newFontFaces();
+    var images = newImages();
+
+    // Default impl options
+    var defaultOptions = {
+        // Default is to fail on error, no placeholder
+        imagePlaceholder: "images/default.png",
+        // Default cache bust is false, it will use the cache
+        cacheBust: true
+    };
+
+    var domtoimage = {
+        toSvg: toSvg,
+        toPng: toPng,
+        toJpeg: toJpeg,
+        toBlob: toBlob,
+        toPixelData: toPixelData,
+        impl: {
+            fontFaces: fontFaces,
+            images: images,
+            util: util,
+            inliner: inliner,
+            options: {}
+        }
+    };
+
+    if (typeof module !== 'undefined')
+        module.exports = domtoimage;
+    else
+        global.domtoimage = domtoimage;
+
+
+    /**
+     * @param {Node} node - The DOM Node object to render
+     * @param {Object} options - Rendering options
+     * @param {Function} options.filter - Should return true if passed node should be included in the output
+     *          (excluding node means excluding it's children as well). Not called on the root node.
+     * @param {String} options.bgcolor - color for the background, any valid CSS color value.
+     * @param {Number} options.width - width to be applied to node before rendering.
+     * @param {Number} options.height - height to be applied to node before rendering.
+     * @param {Object} options.style - an object whose properties to be copied to node's style before rendering.
+     * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only),
+                defaults to 1.0.
+     * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch
+     * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url
+     * @return {Promise} - A promise that is fulfilled with a SVG image data URL
+     * */
+    function toSvg(node, options) {
+        options = options || {};
+        copyOptions(options);
+        return Promise.resolve(node)
+            .then(function (node) {
+                return cloneNode(node, options.filter, true);
+            })
+            .then(embedFonts)
+            .then(inlineImages)
+            .then(applyOptions)
+            .then(function (clone) {
+                return makeSvgDataUri(clone,
+                    options.width || util.width(node),
+                    options.height || util.height(node)
+                );
+            });
+
+        function applyOptions(clone) {
+            if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;
+
+            if (options.width) clone.style.width = options.width + 'px';
+            if (options.height) clone.style.height = options.height + 'px';
+
+            if (options.style)
+                Object.keys(options.style).forEach(function (property) {
+                    clone.style[property] = options.style[property];
+                });
+
+            return clone;
+        }
+    }
+
+    /**
+     * @param {Node} node - The DOM Node object to render
+     * @param {Object} options - Rendering options, @see {@link toSvg}
+     * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data.
+     * */
+    function toPixelData(node, options) {
+        return draw(node, options || {})
+            .then(function (canvas) {
+                return canvas.getContext('2d').getImageData(
+                    0,
+                    0,
+                    util.width(node),
+                    util.height(node)
+                ).data;
+            });
+    }
+
+    /**
+     * @param {Node} node - The DOM Node object to render
+     * @param {Object} options - Rendering options, @see {@link toSvg}
+     * @return {Promise} - A promise that is fulfilled with a PNG image data URL
+     * */
+    function toPng(node, options) {
+        return draw(node, options || {})
+            .then(function (canvas) {
+                return canvas.toDataURL();
+            });
+    }
+
+    /**
+     * @param {Node} node - The DOM Node object to render
+     * @param {Object} options - Rendering options, @see {@link toSvg}
+     * @return {Promise} - A promise that is fulfilled with a JPEG image data URL
+     * */
+    function toJpeg(node, options) {
+        options = options || {};
+        return draw(node, options)
+            .then(function (canvas) {
+                return canvas.toDataURL('image/jpeg', options.quality || 1.0);
+            });
+    }
+
+    /**
+     * @param {Node} node - The DOM Node object to render
+     * @param {Object} options - Rendering options, @see {@link toSvg}
+     * @return {Promise} - A promise that is fulfilled with a PNG image blob
+     * */
+    function toBlob(node, options) {
+        return draw(node, options || {})
+            .then(util.canvasToBlob);
+    }
+
+    function copyOptions(options) {
+        // Copy options to impl options for use in impl
+        if(typeof(options.imagePlaceholder) === 'undefined') {
+            domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder;
+        } else {
+            domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder;
+        }
+
+        if(typeof(options.cacheBust) === 'undefined') {
+            domtoimage.impl.options.cacheBust = defaultOptions.cacheBust;
+        } else {
+            domtoimage.impl.options.cacheBust = options.cacheBust;
+        }
+    }
+
+    function draw(domNode, options) {
+
+        return toSvg(domNode, options)
+            .then(util.makeImage)
+            .then(util.delay(100))
+            .then(function (image) {
+                console.log(image)
+                var canvas = newCanvas(domNode);
+                canvas.getContext('2d').drawImage(image, 0, 0);
+                return canvas;
+            });
+
+        function newCanvas(domNode) {
+            var canvas = document.createElement('canvas');
+            canvas.width = options.width || util.width(domNode);
+            canvas.height = options.height || util.height(domNode);
+
+            if (options.bgcolor) {
+                var ctx = canvas.getContext('2d');
+                ctx.fillStyle = options.bgcolor;
+                ctx.fillRect(0, 0, canvas.width, canvas.height);
+            }
+
+            return canvas;
+        }
+    }
+
+    function cloneNode(node, filter, root) {
+        if (!root && filter && !filter(node)) return Promise.resolve();
+
+        return Promise.resolve(node)
+            .then(makeNodeCopy)
+            .then(function (clone) {
+                return cloneChildren(node, clone, filter);
+            })
+            .then(function (clone) {
+                return processClone(node, clone);
+            });
+
+        function makeNodeCopy(node) {
+            if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL());
+            return node.cloneNode(false);
+        }
+
+        function cloneChildren(original, clone, filter) {
+            var children = original.childNodes;
+            if (children.length === 0) return Promise.resolve(clone);
+
+            return cloneChildrenInOrder(clone, util.asArray(children), filter)
+                .then(function () {
+                    return clone;
+                });
+
+            function cloneChildrenInOrder(parent, children, filter) {
+                var done = Promise.resolve();
+                children.forEach(function (child) {
+                    done = done
+                        .then(function () {
+                            return cloneNode(child, filter);
+                        })
+                        .then(function (childClone) {
+                            if (childClone) parent.appendChild(childClone);
+                        });
+                });
+                return done;
+            }
+        }
+
+        function processClone(original, clone) {
+            if (!(clone instanceof Element)) return clone;
+
+            return Promise.resolve()
+                .then(cloneStyle)
+                .then(clonePseudoElements)
+                .then(copyUserInput)
+                .then(fixSvg)
+                .then(function () {
+                    return clone;
+                });
+
+            function cloneStyle() {
+                copyStyle(window.getComputedStyle(original), clone.style);
+
+                function copyStyle(source, target) {
+                    if (source.cssText) target.cssText = source.cssText;
+                    else copyProperties(source, target);
+
+                    function copyProperties(source, target) {
+                        util.asArray(source).forEach(function (name) {
+                            target.setProperty(
+                                name,
+                                source.getPropertyValue(name),
+                                source.getPropertyPriority(name)
+                            );
+                        });
+                    }
+                }
+            }
+
+            function clonePseudoElements() {
+                [':before', ':after'].forEach(function (element) {
+                    clonePseudoElement(element);
+                });
+
+                function clonePseudoElement(element) {
+                    var style = window.getComputedStyle(original, element);
+                    var content = style.getPropertyValue('content');
+
+                    if (content === '' || content === 'none') return;
+
+                    var className = util.uid();
+                    clone.className = clone.className + ' ' + className;
+                    var styleElement = document.createElement('style');
+                    styleElement.appendChild(formatPseudoElementStyle(className, element, style));
+                    clone.appendChild(styleElement);
+
+                    function formatPseudoElementStyle(className, element, style) {
+                        var selector = '.' + className + ':' + element;
+                        var cssText = style.cssText ? formatCssText(style) : formatCssProperties(style);
+                        return document.createTextNode(selector + '{' + cssText + '}');
+
+                        function formatCssText(style) {
+                            var content = style.getPropertyValue('content');
+                            return style.cssText + ' content: ' + content + ';';
+                        }
+
+                        function formatCssProperties(style) {
+
+                            return util.asArray(style)
+                                .map(formatProperty)
+                                .join('; ') + ';';
+
+                            function formatProperty(name) {
+                                return name + ': ' +
+                                    style.getPropertyValue(name) +
+                                    (style.getPropertyPriority(name) ? ' !important' : '');
+                            }
+                        }
+                    }
+                }
+            }
+
+            function copyUserInput() {
+                if (original instanceof HTMLTextAreaElement) clone.innerHTML = original.value;
+                if (original instanceof HTMLInputElement) clone.setAttribute("value", original.value);
+            }
+
+            function fixSvg() {
+                if (!(clone instanceof SVGElement)) return;
+                clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
+
+                if (!(clone instanceof SVGRectElement)) return;
+                ['width', 'height'].forEach(function (attribute) {
+                    var value = clone.getAttribute(attribute);
+                    if (!value) return;
+
+                    clone.style.setProperty(attribute, value);
+                });
+            }
+        }
+    }
+
+    function embedFonts(node) {
+        return fontFaces.resolveAll()
+            .then(function (cssText) {
+                var styleNode = document.createElement('style');
+                node.appendChild(styleNode);
+                styleNode.appendChild(document.createTextNode(cssText));
+                return node;
+            });
+    }
+
+    function inlineImages(node) {
+        return images.inlineAll(node)
+            .then(function () {
+                return node;
+            });
+    }
+
+    function makeSvgDataUri(node, width, height) {
+        return Promise.resolve(node)
+            .then(function (node) {
+                node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
+                return new XMLSerializer().serializeToString(node);
+            })
+            .then(util.escapeXhtml)
+            .then(function (xhtml) {
+                return '<foreignObject x="0" y="0" width="100%" height="100%">' + xhtml + '</foreignObject>';
+            })
+            .then(function (foreignObject) {
+                return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '">' +
+                    foreignObject + '</svg>';
+            })
+            .then(function (svg) {
+                return 'data:image/svg+xml;charset=utf-8,' + svg;
+            });
+    }
+
+    function newUtil() {
+        return {
+            escape: escape,
+            parseExtension: parseExtension,
+            mimeType: mimeType,
+            dataAsUrl: dataAsUrl,
+            isDataUrl: isDataUrl,
+            canvasToBlob: canvasToBlob,
+            resolveUrl: resolveUrl,
+            getAndEncode: getAndEncode,
+            uid: uid(),
+            delay: delay,
+            asArray: asArray,
+            escapeXhtml: escapeXhtml,
+            makeImage: makeImage,
+            width: width,
+            height: height
+        };
+
+        function mimes() {
+            /*
+             * Only WOFF and EOT mime types for fonts are 'real'
+             * see http://www.iana.org/assignments/media-types/media-types.xhtml
+             */
+            var WOFF = 'application/font-woff';
+            var JPEG = 'image/jpeg';
+
+            return {
+                'woff': WOFF,
+                'woff2': WOFF,
+                'ttf': 'application/font-truetype',
+                'eot': 'application/vnd.ms-fontobject',
+                'png': 'image/png',
+                'jpg': JPEG,
+                'jpeg': JPEG,
+                'gif': 'image/gif',
+                'tiff': 'image/tiff',
+                'svg': 'image/svg+xml'
+            };
+        }
+
+        function parseExtension(url) {
+            var match = /\.([^\.\/]*?)$/g.exec(url);
+            if (match) return match[1];
+            else return '';
+        }
+
+        function mimeType(url) {
+            var extension = parseExtension(url).toLowerCase();
+            return mimes()[extension] || '';
+        }
+
+        function isDataUrl(url) {
+            return url.search(/^(data:)/) !== -1;
+        }
+
+        function toBlob(canvas) {
+            return new Promise(function (resolve) {
+                var binaryString = window.atob(canvas.toDataURL().split(',')[1]);
+                var length = binaryString.length;
+                var binaryArray = new Uint8Array(length);
+
+                for (var i = 0; i < length; i++)
+                    binaryArray[i] = binaryString.charCodeAt(i);
+
+                resolve(new Blob([binaryArray], {
+                    type: 'image/png'
+                }));
+            });
+        }
+
+        function canvasToBlob(canvas) {
+            if (canvas.toBlob)
+                return new Promise(function (resolve) {
+                    canvas.toBlob(resolve);
+                });
+
+            return toBlob(canvas);
+        }
+
+        function resolveUrl(url, baseUrl) {
+            var doc = document.implementation.createHTMLDocument();
+            var base = doc.createElement('base');
+            doc.head.appendChild(base);
+            var a = doc.createElement('a');
+            doc.body.appendChild(a);
+            base.href = baseUrl;
+            a.href = url;
+            return a.href;
+        }
+
+        function uid() {
+            var index = 0;
+
+            return function () {
+                return 'u' + fourRandomChars() + index++;
+
+                function fourRandomChars() {
+                    /* see http://stackoverflow.com/a/6248722/2519373 */
+                    return ('0000' + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4);
+                }
+            };
+        }
+
+        function makeImage(uri) {
+            return new Promise(function (resolve, reject) {
+                var image = new Image();
+                image.onload = function () {
+                    resolve(image);
+                };
+                image.onerror = reject;
+                image.src = uri;
+            });
+        }
+
+        function getAndEncode(url) {
+            var TIMEOUT = 30000;
+            if(domtoimage.impl.options.cacheBust) {
+                // Cache bypass so we dont have CORS issues with cached images
+                // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
+                url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime();
+            }
+
+            return new Promise(function (resolve) {
+                var request = new XMLHttpRequest();
+
+                request.onreadystatechange = done;
+                request.ontimeout = timeout;
+                request.responseType = 'blob';
+                request.timeout = TIMEOUT;
+                request.open('GET', url, true);
+                request.send();
+
+                var placeholder;
+                if(domtoimage.impl.options.imagePlaceholder) {
+                    var split = domtoimage.impl.options.imagePlaceholder.split(/,/);
+                    if(split && split[1]) {
+                        placeholder = split[1];
+                    }
+                }
+
+                function done() {
+                    if (request.readyState !== 4) return;
+
+                    if (request.status !== 200) {
+                        if(placeholder) {
+                            resolve(placeholder);
+                        } else {
+                            fail('cannot fetch resource: ' + url + ', status: ' + request.status);
+                        }
+
+                        return;
+                    }
+
+                    var encoder = new FileReader();
+                    encoder.onloadend = function () {
+                        var content = encoder.result.split(/,/)[1];
+                        resolve(content);
+                    };
+                    encoder.readAsDataURL(request.response);
+                }
+
+                function timeout() {
+                    if(placeholder) {
+                        resolve(placeholder);
+                    } else {
+                        fail('timeout of ' + TIMEOUT + 'ms occured while fetching resource: ' + url);
+                    }
+                }
+
+                function fail(message) {
+                    console.error(message);
+                    resolve('');
+                }
+            });
+        }
+
+        function dataAsUrl(content, type) {
+            return 'data:' + type + ';base64,' + content;
+        }
+
+        function escape(string) {
+            return string.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1');
+        }
+
+        function delay(ms) {
+            return function (arg) {
+                return new Promise(function (resolve) {
+                    setTimeout(function () {
+                        resolve(arg);
+                    }, ms);
+                });
+            };
+        }
+
+        function asArray(arrayLike) {
+            var array = [];
+            var length = arrayLike.length;
+            for (var i = 0; i < length; i++) array.push(arrayLike[i]);
+            return array;
+        }
+
+        function escapeXhtml(string) {
+            return string.replace(/#/g, '%23').replace(/\n/g, '%0A');
+        }
+
+        function width(node) {
+            var leftBorder = px(node, 'border-left-width');
+            var rightBorder = px(node, 'border-right-width');
+            return node.scrollWidth + leftBorder + rightBorder;
+        }
+
+        function height(node) {
+            var topBorder = px(node, 'border-top-width');
+            var bottomBorder = px(node, 'border-bottom-width');
+            return node.scrollHeight + topBorder + bottomBorder;
+        }
+
+        function px(node, styleProperty) {
+            var value = window.getComputedStyle(node).getPropertyValue(styleProperty);
+            return parseFloat(value.replace('px', ''));
+        }
+    }
+
+    function newInliner() {
+        var URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
+
+        return {
+            inlineAll: inlineAll,
+            shouldProcess: shouldProcess,
+            impl: {
+                readUrls: readUrls,
+                inline: inline
+            }
+        };
+
+        function shouldProcess(string) {
+            return string.search(URL_REGEX) !== -1;
+        }
+
+        function readUrls(string) {
+            var result = [];
+            var match;
+
+            while ((match = URL_REGEX.exec(string)) !== null) {
+                result.push(match[1]);
+            }
+            return result.filter(function (url) {
+                return !util.isDataUrl(url);
+            });
+        }
+
+        function inline(string, url, baseUrl, get) {
+            return Promise.resolve(url)
+                .then(function (url) {
+                    return baseUrl ? util.resolveUrl(url, baseUrl) : url;
+                })
+                .then(get || util.getAndEncode)
+                .then(function (data) {
+                    return util.dataAsUrl(data, util.mimeType(url));
+                })
+                .then(function (dataUrl) {
+                    return string.replace(urlAsRegex(url), '$1' + dataUrl + '$3');
+                });
+
+            function urlAsRegex(url) {
+                return new RegExp('(url\\([\'"]?)(' + util.escape(url) + ')([\'"]?\\))', 'g');
+            }
+        }
+
+        function inlineAll(string, baseUrl, get) {
+            if (nothingToInline()) return Promise.resolve(string);
+
+            return Promise.resolve(string)
+                .then(readUrls)
+                .then(function (urls) {
+                    var done = Promise.resolve(string);
+                    urls.forEach(function (url) {
+                        done = done.then(function (string) {
+                            return inline(string, url, baseUrl, get);
+                        });
+                    });
+                    return done;
+                });
+
+            function nothingToInline() {
+                return !shouldProcess(string);
+            }
+        }
+    }
+
+    function newFontFaces() {
+        return {
+            resolveAll: resolveAll,
+            impl: {
+                readAll: readAll
+            }
+        };
+
+        function resolveAll() {
+            return readAll(document)
+                .then(function (webFonts) {
+                    return Promise.all(
+                        webFonts.map(function (webFont) {
+                            return webFont.resolve();
+                        })
+                    );
+                })
+                .then(function (cssStrings) {
+                    return cssStrings.join('\n');
+                });
+        }
+
+        function readAll() {
+            return Promise.resolve(util.asArray(document.styleSheets))
+                .then(getCssRules)
+                .then(selectWebFontRules)
+                .then(function (rules) {
+                    return rules.map(newWebFont);
+                });
+
+            function selectWebFontRules(cssRules) {
+                return cssRules
+                    .filter(function (rule) {
+                        return rule.type === CSSRule.FONT_FACE_RULE;
+                    })
+                    .filter(function (rule) {
+                        console.log(rule.style.getPropertyValue('href'))
+                        return inliner.shouldProcess(rule.style.getPropertyValue('href'));
+                    });
+            }
+
+            function getCssRules(styleSheets) {
+                var cssRules = [];
+                styleSheets.forEach(function (sheet) {
+                    try {
+                        util.asArray(sheet.cssRules || []).forEach(cssRules.push.bind(cssRules));
+                    } catch (e) {
+                        console.log('Error while reading CSS rules from ' + sheet.href, e.toString());
+                    }
+                });
+                return cssRules;
+            }
+
+            function newWebFont(webFontRule) {
+                return {
+                    resolve: function resolve() {
+                        var baseUrl = (webFontRule.parentStyleSheet || {}).href;
+                        return inliner.inlineAll(webFontRule.cssText, baseUrl);
+                    },
+                    src: function () {
+                        return webFontRule.style.getPropertyValue('src');
+                    }
+                };
+            }
+        }
+    }
+
+    function newImages() {
+        return {
+            inlineAll: inlineAll,
+            impl: {
+                newImage: newImage
+            }
+        };
+
+        function newImage(element) {
+            return {
+                inline: inline
+            };
+
+            function inline(get) {
+                if(element.src == undefined) {
+                  var my_str = element.href.animVal;
+                } else {
+                  var my_str = element.src
+                }
+                if (util.isDataUrl(my_str)) return Promise.resolve();
+
+                return Promise.resolve(my_str)
+                    .then(get || util.getAndEncode)
+                    .then(function (data) {
+                        return util.dataAsUrl(data, util.mimeType(my_str));
+                    })
+                    .then(function (dataUrl) {
+                        return new Promise(function (resolve, reject) {
+                            element.onload = resolve;
+                            element.onerror = reject;
+                            element.src = dataUrl;
+                            element.setAttribute('href',dataUrl);
+                        });
+                    });
+            }
+        }
+
+        function inlineAll(node) {
+            if (!(node instanceof Element)) return Promise.resolve(node);
+
+            return inlineBackground(node)
+                .then(function () {
+                    if (node instanceof HTMLImageElement || node instanceof SVGImageElement)
+                        return newImage(node).inline();
+                    else
+                        return Promise.all(
+                            util.asArray(node.childNodes).map(function (child) {
+                                return inlineAll(child);
+                            })
+                        );
+                });
+
+            function inlineBackground(node) {
+                var background = node.style.getPropertyValue('background');
+
+                if (!background) return Promise.resolve(node);
+
+                return inliner.inlineAll(background)
+                    .then(function (inlined) {
+                        node.style.setProperty(
+                            'background',
+                            inlined,
+                            node.style.getPropertyPriority('background')
+                        );
+                    })
+                    .then(function () {
+                        return node;
+                    });
+            }
+        }
+    }
+})(this);

+ 101 - 0
TEAMModelOS/ClientApp/src/utils/html2pdf.js

@@ -0,0 +1,101 @@
+// 导出页面为PDF格式
+import JsPDF from 'jspdf'
+import domtoimage from './dom_to_image';
+export default {
+    install(Vue, options) {
+        Vue.prototype.getPdfByImg = () => {
+
+        }
+
+        Vue.prototype.getPdf = () => {
+            var title = '测试答题卡'
+            setTimeout(() => {
+                domtoimage.toJpeg(document.querySelector('#pdfDom'), { bgcolor: '#fff', scale: 4 })
+                    .then((pageData) => {
+                        let img = new Image();
+                        img.src = pageData;
+                        img.onload = function () {
+                            console.log('height', img.height);
+                            console.log('width', img.width);
+                            const a4Height = 841.89
+                            const a4Width = 592.28
+                            let contentWidth = img.width
+                            let contentHeight = img.height
+                            let pageHeight = contentWidth / a4Width * a4Height
+                            let leftHeight = contentHeight
+                            let position = 0
+                            let imgWidth = a4Width
+                            let imgHeight = a4Width / contentWidth * contentHeight
+                            console.log(pageData)
+                            console.log(pageHeight, leftHeight)
+                            let PDF = new JsPDF({
+                                unit: 'pt',
+                                format: 'a4',
+                                putOnlyUsedFonts: true,
+                                floatPrecision: 16
+                            })
+                            // 如果是一页的情况
+                            if (leftHeight <= pageHeight) {
+                                PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
+                            } else {
+                                // 如果超出则多页
+                                while (leftHeight > 0) {
+                                    PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
+                                    leftHeight -= pageHeight
+                                    position -= a4Height
+                                    if (leftHeight > 0) {
+                                        PDF.addPage()
+                                    }
+                                }
+                            }
+                            PDF.save(title + '.pdf')
+                        }
+
+                    })
+                    .catch(function (error) {
+                        console.error('oops, something went wrong!', error);
+                    });
+
+                // html2canvas( document.querySelector('#pdfDom'), {
+                //     allowTaint: true,  //允许 canvas 污染, allowTaint参数要去掉,否则是无法通过toDataURL导出canvas数据的
+                //     useCORS:true  //允许canvas画布内 可以跨域请求外部链接图片, 允许跨域请求。
+                // } ).then( (canvas)=>{
+                //     var contentWidth = canvas.width;
+                //     var contentHeight = canvas.height;
+                //     console.log(contentWidth)
+                //     console.log(contentHeight)
+                //     //一页pdf显示html页面生成的canvas高度;
+                //     var pageHeight = contentWidth / 592.28 * 841.89;
+                //     //未生成pdf的html页面高度
+                //     var leftHeight = contentHeight;
+                //     //页面偏移
+                //     var position = 0;
+                //     //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
+                //     var imgWidth = 595.28;
+                //     var imgHeight = 595.28/contentWidth * contentHeight;
+                //     var pageData = canvas.toDataURL('image/jpeg', 1.0);
+                //     var pdf = new JsPDF('', 'pt', 'a4');
+                //     //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
+                //     //当内容未超过pdf一页显示的范围,无需分页
+                //     if (leftHeight < pageHeight) {
+                //         //在pdf.addImage(pageData, 'JPEG', 左,上,宽度,高度)设置在pdf中显示;
+                //         pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
+                //         // pdf.addImage(pageData, 'JPEG', 20, 40, imgWidth, imgHeight);
+                //     } else {    // 分页
+                //         while(leftHeight > 0) {
+                //             pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
+                //             leftHeight -= pageHeight;
+                //             position -= 841.89;
+                //             //避免添加空白页
+                //             if(leftHeight > 0) {
+                //                 pdf.addPage();
+                //             }
+                //         }
+                //     }
+                //     //可动态生成
+                //     pdf.save(title + '.pdf')
+                // })
+            }, 1000)
+        }
+    }
+}

+ 371 - 0
TEAMModelOS/ClientApp/src/utils/items.json

@@ -0,0 +1,371 @@
+[
+    {
+        "id":1,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":2,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":3,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":4,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":5,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":6,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":7,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":8,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":9,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":10,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":11,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":12,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":13,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":14,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "single",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":15,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":5,
+        "answer": [],
+        "explain": null,
+        "type": "judge",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":16,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "multiple",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":17,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1],
+        "answer": [],
+        "explain": null,
+        "type": "multiple",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":18,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1],
+        "answer": [],
+        "explain": null,
+        "type": "multiple",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":19,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":20,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":21,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":22,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":23,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":24,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":1,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":25,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "complete",
+        "scope": "school",
+        "score":8
+    },
+    
+    {
+        "id":26,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":27,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":28,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":29,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":30,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":31,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":32,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":33,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":34,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    },
+    {
+        "id":35,
+        "question": " 下列语句中加点的成语使用不正确的一项是",
+        "option": [1,1,1,1,1],
+        "blankCount":2,
+        "answer": [],
+        "explain": null,
+        "type": "subjective",
+        "scope": "school",
+        "score":8
+    }
+]

+ 63 - 0
TEAMModelOS/ClientApp/src/utils/sheetConfig.js

@@ -0,0 +1,63 @@
+const PAPER_W = 750; // 答题卡宽度
+const PAPER_H = 1060.71; // 答题卡高度
+const ANCHORPROP = {
+    width: 10, // 锚点宽度
+    height: 10, // 锚点高度
+    gap: 40, // 锚点间距
+    gapX:20,
+    gapY:40
+};
+const SVG_BORDER_PROP = {
+    x:55,
+    y:65,
+    width:PAPER_W - 55 * 2,
+    height:925
+}
+const CONTENT_MT = 70; // 内容与定位框间隔
+const CONTENT_ML = 50; // 内容与定位框间隔
+
+const CONTENT_START_X = ANCHORPROP.width + ANCHORPROP.gapX + CONTENT_ML; // 内容开始x坐标
+const CONTENT_START_Y = ANCHORPROP.height + ANCHORPROP.gapY + CONTENT_MT; // 内容开始y坐标
+
+const INFO_W = PAPER_W - CONTENT_START_X * 2; // 信息框内容宽度
+const INFO_H = 200; // 信息框内容高度
+const INFO_ITEM_GAP = 20; // 个人信息填写区域ITEM上下间隔
+const INFO_ITEM_MARGIN = 40; //个人信息填写区域坐标间隔
+const ID_LENGTH = 8; // 准考证号位数
+const INFO_LEFT_X = 400; // 信息左边X值
+const INFO_LEFT_W = INFO_LEFT_X - CONTENT_ML - ANCHORPROP.width - ANCHORPROP.gapX; // 信息左边框宽度
+const ID_TITLE_H = 40; // 准考证号框高度
+const ID_TITLE_Y = ID_TITLE_H + CONTENT_MT + ANCHORPROP.height + ANCHORPROP.gapY; // 准考证号框Y值
+const NUMBER_CELL_H = 30; // 准考证号框单元格高度
+const NUMBER_ITEM_W = 15.5; // 准考证号填涂宽度
+const NUMBER_ITEM_H = 9.5; // 准考证号填涂高度
+const NUMBER_ITEM_MT = (INFO_H - ID_TITLE_H - NUMBER_CELL_H) / 10.5; // 准考证号填涂上间距
+const NUMBER_ITEM_ML = 5; // 准考证号填涂左间距
+
+const BLOCK_H = 120
+
+export {
+    PAPER_W,
+    PAPER_H,
+    BLOCK_H,
+    ANCHORPROP,
+    CONTENT_MT,
+    CONTENT_ML,
+    CONTENT_START_X,
+    CONTENT_START_Y,
+    INFO_W,
+    INFO_H,
+    INFO_ITEM_GAP,
+    INFO_ITEM_MARGIN,
+    ID_LENGTH,
+    INFO_LEFT_X,
+    INFO_LEFT_W,
+    ID_TITLE_H,
+    ID_TITLE_Y,
+    NUMBER_CELL_H,
+    NUMBER_ITEM_W,
+    NUMBER_ITEM_H,
+    NUMBER_ITEM_MT,
+    NUMBER_ITEM_ML,
+    SVG_BORDER_PROP
+}

+ 422 - 0
TEAMModelOS/ClientApp/src/view/answersheet/BaseEditor.vue

@@ -0,0 +1,422 @@
+<template>
+  <div>
+    <div :id="ids" class="sheet-Editor"></div>
+    <div
+      :id="ids + 'fix'"
+      class="sheet-Editor"
+      v-show="type !== '0' && isShowFixEditor(ids)"
+    ></div>
+  </div>
+</template>
+
+<script>
+import E from "wangeditor";
+import {
+  PAPER_W,
+  PAPER_H,
+  SVG_BORDER_PROP,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "../../utils/sheetConfig.js";
+export default {
+  props: {
+    ids: {
+      type: String,
+      default: "",
+    },
+    items: {
+      type: Array,
+      default: () => [],
+    },
+    allItems: {
+      type: Array,
+      default: () => [],
+    },
+    type: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      myEditor: null,
+      myFixEditor: null,
+      completeItems: [],
+      isNewPage: false,
+      pageCount: 1,
+      curEditorSetting:null
+    };
+  },
+  methods: {
+    // 渲染当前富文本
+    doRenderEditor(items) {
+      console.log('富文本框进入渲染');
+      switch (this.type) {
+        case "0":
+          this.doInsertComplete(items);
+          break;
+        case "1":
+          this.doInsertSubjective(items);
+          break;
+        default:
+          break;
+      }
+    },
+    // 填空题型富文本框
+    doInsertComplete(items) {
+      // console.log(this.items);
+      let underLineSpaceCount = 45;
+      items.forEach((item, index) => {
+        let addStr = this.items.indexOf(item) + 1 + "";
+        for (let blankIndex = 0; blankIndex < item.blankCount; blankIndex++) {
+          addStr =
+            addStr +
+            '<span class="underline">' +
+            new Array(underLineSpaceCount).fill("&nbsp;").join("") +
+            "</span>";
+        }
+        this.$nextTick(() => {
+          this.myEditor.txt.append(
+            `<span class='complete-item'>${addStr}</span>`
+          );
+        });
+      });
+    },
+
+    // 渲染问答题
+    doInsertSubjective(items) {
+      let subjectiveItem = items[0];
+      let itemOrder = this.allItems.indexOf(subjectiveItem) + 1;
+      let addStr = itemOrder + "(" + (subjectiveItem.score + "分)"); // 渲染题号
+      let defaultBrCounts = 1; // 问答题默认回答区域空行数量
+      let brHtml = new Array(defaultBrCounts).fill("<p><br></p>").join("");
+      this.$nextTick(() => {
+        setTimeout(() => {
+          let isNewPage = this.$store.state.answerSheet.isNewPage;
+          let lastBottomGap = 20;
+          let Y =
+            this.myEditor.$textElem.elems[0].getBoundingClientRect().top - 20;
+          let paperH = PAPER_H;
+          let curEditorY = Y > paperH ? Y % paperH : Y;
+          let curEditorH = this.myEditor.$textElem.elems[0].clientHeight; // 200px
+          // console.log("==========================");
+          // console.log(itemOrder, this.myEditor.$textElem.elems[0]);
+          // console.log(
+          //   itemOrder,
+          //   this.myEditor.$textElem.elems[0].getBoundingClientRect()
+          // );
+          // console.log(itemOrder, Y);
+          // console.log(itemOrder, curEditorY);
+          // console.log(itemOrder, curEditorH);
+          // console.log(itemOrder, isNewPage);
+          // console.log(
+          //   itemOrder,
+          //   curEditorY - SVG_BORDER_PROP.y + curEditorH + lastBottomGap
+          // );
+          console.log('2222222222');
+          let curEditorDom = document.getElementById(this.ids);
+          if (
+            curEditorY - SVG_BORDER_PROP.y + curEditorH + lastBottomGap >
+            SVG_BORDER_PROP.height
+          ) {
+            let renderHeight =
+              SVG_BORDER_PROP.height -
+              (curEditorY - SVG_BORDER_PROP.y) -
+              lastBottomGap;
+            if (!isNewPage) {
+              // 如果渲染的客观题高度在这个区间 才需要在下一页添加补充作答区域 其余全部按照正常 跨页处理不需要补充作答区域
+              if (renderHeight > 100 && renderHeight < 160) {
+                console.log(itemOrder + "需要处理高度");
+                this.$store.commit("addFixItem", itemOrder);
+                this.myEditor.config.height = renderHeight;
+                document.getElementById(this.ids + "fix").style.marginTop =
+                  PAPER_H -
+                  SVG_BORDER_PROP.height -
+                  SVG_BORDER_PROP.y +
+                  SVG_BORDER_PROP.y +
+                  40 +
+                  "px";
+                this.myFixEditor.config.height = 200 - renderHeight;
+                this.$store.commit("changeNewPage", true);
+                if (itemOrder !== this.allItems.length) {
+                  this.$store.commit("addPage");
+                }
+              }else{
+                // 跨页处理不需要补充作答区域
+                this.$store.commit("changeNewPage", true);
+                document.getElementById(this.ids).style.marginTop =
+                  PAPER_H -
+                  SVG_BORDER_PROP.height -
+                  SVG_BORDER_PROP.y +
+                  SVG_BORDER_PROP.y +
+                  40 + renderHeight + 20 +
+                  "px";
+                  // 把上一个编辑器的高度 填满上一页
+                // let preEditorId = 'subjectiveEditor' + (Number(this.ids.replace('subjectiveEditor','')) - 1)
+                // let preEditorDom = document.getElementById(preEditorId)
+                // if(preEditorDom){
+                //   preEditorDom.getElementsByClassName('w-e-text-container')[0].style.height = (preEditorDom.getElementsByClassName('w-e-text-container')[0].getBoundingClientRect().height + 50) + 'px'
+                // }
+                
+                if (itemOrder !== this.allItems.length) {
+                  this.$store.commit("addPage");
+                }
+              }
+            } else {
+              this.$store.commit("changeNewPage", false);
+            }
+          } else {
+            this.$store.commit("changeNewPage", false);
+          }
+          this.$root.$children[0].spinShow = false;
+
+          // this.myEditor.txt.append('curEditorY:' + curEditorY + 'px');
+          // this.myEditor.txt.append('height:' + this.myEditor.config.height + 'px');
+        }, 30);
+
+        this.myEditor.txt.append(addStr + brHtml);
+      });
+    },
+
+    define({ editor, prop, change, validate }) {
+      if (typeof validate != "function") {
+        validate = function (value) {
+          return {
+            valid: true, // 验证的结果。true: 验证通过;false: 验证失败。
+            data: value, // 处理后的值,不需要处理直接返回即可
+          };
+        };
+      }
+      let temp = editor.config[prop];
+      Object.defineProperty(editor.config, prop, {
+        enumerable: true,
+        configurable: true,
+        get: function () {
+          return temp;
+        },
+        set: function (value) {
+          const { valid, data } = validate(value);
+          if (valid && data !== temp) {
+            temp = data;
+            change(temp);
+          }
+          return temp;
+        },
+      });
+    },
+
+    // 拖拽改变编辑器的高度
+    dragReSize(editor) {
+      let that = this
+      // 将一个 HTML 字符串转换为由 wangEditor.$(一个类 jQuery 的东东) 包裹的实例对象
+      const reel = E.$(
+        '<div style="position: absolute;bottom: 0;left: 0;width: 100%;background-color: none;cursor: n-resize;border-top: 3px solid transparent;"></div>'
+      );
+      // 将这个实例添加到 wangEditor 的编辑区容器中
+      editor.$textContainerElem.append(reel); // 添加鼠标事件
+      reel.on("mousedown", function mousedown(down) {
+        let last = down; // 鼠标移动,更新 wangEditor 实例中的 height 配置项
+        function mousemove(move) {
+          editor.config.height += move.clientY - last.clientY;
+          last = move;
+        } // 在鼠标弹起事件中移除事件监听
+        function mouseup() {
+          document.removeEventListener("mousemove", mousemove);
+          document.removeEventListener("mouseup", mouseup);
+          that.$store.commit('setEditorHeight',{
+            id:that.ids,
+            height:editor.config.height
+          })
+          that.$EventBus.$emit('doRefresh')
+        }
+        // 将鼠标事件绑定到 document 上
+        document.addEventListener("mousemove", mousemove);
+        document.addEventListener("mouseup", mouseup);
+      });
+    },
+
+    equalAltitude({ editor, max, min }) {
+      editor.config.onchange = function () {
+        const last = Array.prototype.slice.call(
+          editor.$textElem.elems[0].children,
+          -1
+        )[0];
+        if (last) {
+          const height = last.offsetTop + last.clientHeight;
+          // editor.config.height = editor.config.height = height < min ? min : height > max ? max : height
+        }
+      };
+    },
+
+    initEditor(curId, isFix) {
+      const editor = new E("#" + curId);
+      editor.config.height = this.type === "0" ? "" : 200;
+
+      this.define({
+        editor: editor,
+        prop: "height",
+        change(value) {
+          editor.$textContainerElem.css("height", `${value}px`);
+        },
+        validate(value) {
+          const data = parseFloat(value);
+          return {
+            valid: !isNaN(data), // 数据校验:设置的值只能为数字
+            data: data, // 此处返回的是被处理后的数据
+          };
+        },
+      });
+      // 因为动态更新编辑器高度涉及到 wangEditor 的 onchange 回调,
+      // 而 onchange 必须在 create 前的配置才有效,所以放在 create 前。
+      this.equalAltitude({ editor: editor });
+
+      editor.config.menus = [
+        "bold",
+        "fontSize",
+        "fontName",
+        "italic",
+        "underline",
+        "strikeThrough",
+        "indent",
+        "lineHeight",
+        "foreColor",
+        "backColor",
+        "undo",
+      ];
+
+      editor.config.onfocus = (html) => {
+        document
+          .getElementById(curId)
+          .getElementsByClassName("w-e-toolbar")[0].style.display = "flex";
+      };
+
+      editor.config.onblur = (html) => {
+        document
+          .getElementById(curId)
+          .getElementsByClassName("w-e-toolbar")[0].style.display = "none";
+      };
+      editor.config.focus = false;
+      editor.config.showFullScreen = false;
+      editor.config.placeholder = "";
+      let curSetting = this.hasEditorSetting(curId)
+      if(curSetting && Object.keys(curSetting).length){
+        console.log('从vuex读取的数据',curId,curSetting.height)
+        editor.config.height = curSetting.height
+      }
+      editor.create();
+      this.dragReSize(editor);
+      if (!isFix) {
+        this.myEditor = editor;
+      } else {
+        this.myFixEditor = editor;
+      }
+    },
+  },
+  mounted() {
+    this.initEditor(this.ids);
+    this.initEditor(this.ids + "fix", true);
+  },
+  computed: {
+    // 是否需要展示跨页展示的富文本框
+    isShowFixEditor() {
+      return (ids) => {
+        let itemOrder = this.allItems.indexOf(this.curPropItem[0]) + 1;
+        return this.$store.state.answerSheet.needFixArr.includes(+itemOrder);
+      };
+    },
+    // 是否有vuex设定高度值
+    hasEditorSetting(){
+      return ids => {
+        return this.$store.state.answerSheet.editorInfos[ids]
+      }
+    }
+
+  },
+  watch: {
+    items: {
+      handler(n, o) {
+        if (n.length) {
+          let objectiveArr = ["complete"];
+          this.completeItems = n.filter((item) =>
+            objectiveArr.includes(item.type)
+          );
+          this.curPropItem = this.type === "0" ? this.completeItems : n;
+          this.doRenderEditor(this.curPropItem);
+        }
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style>
+.sheet-Editor {
+  position: relative;
+  width: 590px;
+  margin: 0 auto 20px auto;
+  border: 1.2px solid #000 !important;
+}
+
+.sheet-Editor .w-e-toolbar {
+  display: none;
+  position: absolute;
+  top: -32px;
+  left: -1px;
+  width: 590px;
+  border: 1px solid #000 !important;
+  border-bottom: none !important;
+}
+
+.sheet-Editor .w-e-text-container {
+  border: none !important;
+  z-index: 1 !important;
+}
+
+.sheet-Editor .w-e-text {
+  padding: 10px;
+  font-size: 12px;
+  font-family: "Times New Roman", "宋体";
+}
+
+.sheet-Editor .w-e-toolbar .w-e-menu {
+  width: 30px;
+  height: 30px;
+}
+
+.outerBox {
+  border: 1px solid #000;
+  width: 590px;
+  height: 100px;
+  margin: 0 auto 20px auto;
+}
+
+.sheet-Editor .underline {
+  /* text-decoration: underline; */
+  border-bottom: 1px solid #333;
+  margin: 10px;
+  display: inline-block;
+}
+
+.sheet-Editor .complete-item {
+  margin: 0 4px;
+  display: inline-block;
+}
+</style>

+ 103 - 0
TEAMModelOS/ClientApp/src/view/answersheet/BaseObjective.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="baseSingle">
+    <svg :id="ids"></svg>
+  </div>
+</template>
+ 
+<script>
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import {
+  PAPER_W,
+  PAPER_H,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    exerciseItem: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+    ids: {
+      type: String,
+      default: "",
+    },
+    exerciseIndex: {
+      type: Number,
+      default: 0,
+    },
+  },
+  components: {},
+  data() {
+    return {
+      isShowInfoEdit: false,
+      editModal: false,
+      infoLeftBox: null,
+      idNumberBox: null,
+      snap: null,
+    };
+  },
+  created() {},
+  methods: {
+    doRender(exerciseItem) {
+      //   this.snap = Snap("#" + this.ids);
+      var snap = this.snap;
+      let optionGap = 30;
+      snap
+        .text(0, 20, this.exerciseIndex)
+        .attr({ fontFamily: 'Times New Roman' });
+      for (let index = 0; index < exerciseItem.option.length; index++) {
+        let code = String.fromCharCode(64 + parseInt(index + 1)).toLowerCase();
+        let img = require("../../icons/answersheet/" + code + ".svg");
+        let c3 = snap
+          .image(img, 20 + optionGap * index, 10, NUMBER_ITEM_W, NUMBER_ITEM_H)
+          .attr({
+            crossorigin: "anonymous",
+          });
+      }
+    },
+  },
+  mounted: function () {
+    this.snap = Snap("#" + this.ids);
+    var snap = this.snap;
+  },
+  watch: {
+    exerciseItem: {
+      handler(n) {
+        if (Object.keys(n).length) {
+          this.$nextTick(() => {
+            this.doRender(n);
+          });
+        }
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+<style scoped>
+.baseSingle svg {
+  width: 100%;
+  height: 20px;
+}
+</style>
+ 

+ 151 - 0
TEAMModelOS/ClientApp/src/view/answersheet/BaseSvgBg.vue

@@ -0,0 +1,151 @@
+<template>
+  <div style="width: 100%; height: 100%; overflow: hidden; margin-top: 0;">
+    <svg :id="ids" width="100%" height="100%" style="background: #fff"></svg>
+  </div>
+</template>
+
+<script>
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import { PAPER_W, PAPER_H, ANCHORPROP, SVG_BORDER_PROP } from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    ids: {
+      type: String,
+      default: "",
+    },
+    total: {
+      type: Number,
+      default: 0,
+    },
+  },
+  datas() {
+    return {
+      pageCount: 1,
+      snap: null,
+      pageBox: null,
+      anchorBox: null,
+    };
+  },
+  created() {},
+  methods: {
+    doRenderPageAnchor(snap) {
+      this.anchorBox.remove(); // 先清除之前的绘制内容
+      this.anchorBox = this.snap.paper.g();
+      let curPage = Number(this.ids.replace("svg", ""));
+      // 将当前页码转换成二进制进行页码锚点绘制
+      let i = (curPage).toString(2)
+      let radixArr = i.length === 1 ? '00' + i : i.length === 2 ? '0' + i : i
+      // 绘制锚点
+      for (let index = 0; index < 3; index++) {
+        this.anchorBox.add(
+          snap
+            .rect(
+              ANCHORPROP.gapX + 55 + 30 * index,
+              PAPER_H - ANCHORPROP.height - ANCHORPROP.gapY,
+              20,
+              10
+            )
+            .attr({ fill: radixArr.split('')[index]  === '1' ? '#000' : "none", stroke: "#000", strokeWidth: 1 })
+        );
+      }
+    },
+
+    doRenderPage(snap) {
+      this.pageBox.remove(); // 先清除之前的绘制内容
+      this.pageBox = this.snap.paper.g();
+      // 页码
+      var page = snap
+        .text(
+          (PAPER_W - 100) / 2,
+          PAPER_H - 50,
+          `第 ${Number(this.ids.replace("svg", "")) + 1} 页  /  共 ${
+            this.total
+          } 页`
+        )
+        .attr({
+          fontSize: 12,
+          fontFamily: "Times New Roman",
+        });
+      this.pageBox.add(page);
+    },
+  },
+  mounted() {
+    var snap = Snap("#" + this.ids);
+    this.pageBox = snap.paper.g();
+    this.anchorBox = snap.paper.g();
+    let anchorProp = ANCHORPROP;
+    // snap
+    //   .rect(SVG_BORDER_PROP.x, SVG_BORDER_PROP.y, SVG_BORDER_PROP.width, SVG_BORDER_PROP.height)
+    //   .attr({ fill: "none", stroke: "#000", strokeWidth: 1 });
+
+    // 画四个锚点
+    snap.rect(
+      anchorProp.gapX,
+      anchorProp.gapY,
+      anchorProp.width,
+      anchorProp.height
+    );
+    snap.rect(
+      anchorProp.gapX - 5,
+      anchorProp.gapY - 5,
+      anchorProp.width + 10,
+      anchorProp.height + 10
+    ).attr({ fill: "none", stroke: "#000", strokeWidth: 4 });
+    snap.rect(
+      anchorProp.gapX,
+      PAPER_H - anchorProp.height - anchorProp.gapY,
+      anchorProp.width,
+      anchorProp.height
+    );
+    snap.rect(
+      anchorProp.gapX - 5,
+      PAPER_H - anchorProp.height - anchorProp.gapY - 5,
+      anchorProp.width + 10,
+      anchorProp.height + 10
+    ).attr({ fill: "none", stroke: "#000", strokeWidth: 4 });
+    snap.rect(
+      PAPER_W - anchorProp.width - anchorProp.gapX,
+      anchorProp.gapY,
+      anchorProp.width,
+      anchorProp.height
+    );
+    snap.rect(
+      PAPER_W - anchorProp.width - anchorProp.gapX - 5,
+      anchorProp.gapY - 5,
+      anchorProp.width + 10,
+      anchorProp.height + 10
+    ).attr({ fill: "none", stroke: "#000", strokeWidth: 4 });
+    snap.rect(
+      PAPER_W - anchorProp.width - anchorProp.gapX,
+      PAPER_H - anchorProp.height - anchorProp.gapY,
+      anchorProp.width,
+      anchorProp.height
+    );
+    snap.rect(
+      PAPER_W - anchorProp.width - anchorProp.gapX - 5,
+      PAPER_H - anchorProp.height - anchorProp.gapY - 5,
+      anchorProp.width + 10,
+      anchorProp.height + 10
+    ).attr({ fill: "none", stroke: "#000", strokeWidth: 4 });
+    if (this.ids === "svg0") {
+      // 标题
+      snap.text((PAPER_W - 128) / 2, 95, "答题卡自动生成").attr({
+        fontSize: 18,
+        fontWeight: 800,
+      });
+    }
+    this.snap = snap;
+    this.doRenderPage(snap);
+    this.doRenderPageAnchor(snap);
+
+  },
+  watch: {
+    total(n, o) {
+      this.doRenderPage(this.snap);
+    },
+  },
+};
+</script>
+
+<style>
+</style>

+ 330 - 0
TEAMModelOS/ClientApp/src/view/answersheet/BaseTitleEditor.vue

@@ -0,0 +1,330 @@
+<template>
+  <div :id="ids" class="sheet-title-Editor"></div>
+</template>
+
+<script>
+import E from "wangeditor";
+import {
+  PAPER_W,
+  PAPER_H,
+  SVG_BORDER_PROP,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    ids: {
+      type: String,
+      default: "",
+    },
+    content: {
+      type: String,
+      default: "",
+    },
+    items: {
+      type: Array,
+      default: () => [],
+    },
+    allItems: {
+      type: Array,
+      default: () => [],
+    },
+    type: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      myEditor: null,
+      completeItems: [],
+      isNewPage: false,
+      pageCount: 1,
+    };
+  },
+  methods: {
+    doRenderEditor(items) {
+      switch (this.type) {
+        case "0":
+          this.doInsertComplete(items);
+          break;
+        case "1":
+          this.doInsertSubjective(items);
+          break;
+        default:
+          break;
+      }
+    },
+
+    doInsertComplete(items) {
+      // console.log(this.items);
+      let underLineSpaceCount = 45;
+      items.forEach((item, index) => {
+        let addStr = this.items.indexOf(item) + 1 + "";
+        for (let blankIndex = 0; blankIndex < item.blankCount; blankIndex++) {
+          addStr =
+            addStr +
+            '<span class="underline">' +
+            new Array(underLineSpaceCount).fill("&nbsp;").join("") +
+            "</span>";
+        }
+        this.$nextTick(() => {
+          this.myEditor.txt.append(
+            `<span class='complete-item'>${addStr}</span>`
+          );
+        });
+      });
+    },
+
+    // 渲染问答题
+    doInsertSubjective(items) {
+      let subjectiveItem = items[0];
+      let itemOrder = this.allItems.indexOf(subjectiveItem) + 1
+      let addStr = itemOrder + "(" + (subjectiveItem.score+ '分)'); // 渲染题号
+      let defaultBrCounts = 1; // 问答题默认回答区域空行数量
+      let brHtml = new Array(defaultBrCounts).fill("<p><br></p>").join("");
+      this.$nextTick(() => {
+        setTimeout(() => {
+          let isNewPage = this.$store.state.answerSheet.isNewPage
+          let lastBottomGap = 20
+          let Y = this.myEditor.$textElem.elems[0].getBoundingClientRect().top - 20
+          let paperH = PAPER_H
+          let curEditorY = Y > paperH ? Y % paperH : Y ;
+          let curEditorH = this.myEditor.$textElem.elems[0].clientHeight; // 200px
+          console.log('==========================');
+          console.log(itemOrder,this.myEditor.$textElem.elems[0]);
+          console.log(itemOrder,this.myEditor.$textElem.elems[0].getBoundingClientRect());
+          console.log(itemOrder,Y);
+          console.log(itemOrder,curEditorY);
+          console.log(itemOrder,curEditorH);
+          console.log(itemOrder,isNewPage);
+          console.log(itemOrder,curEditorY - SVG_BORDER_PROP.y + curEditorH + lastBottomGap);
+          if (curEditorY - SVG_BORDER_PROP.y + curEditorH + lastBottomGap > SVG_BORDER_PROP.height) {
+            if (!isNewPage) {
+              console.log(itemOrder + '需要处理高度')
+              this.myEditor.config.height = SVG_BORDER_PROP.height - (curEditorY - SVG_BORDER_PROP.y) - lastBottomGap ;
+              // this.myEditor.config.height = 1060.7 - curEditorY - 100 ;
+              this.$store.commit('changeNewPage',true)
+              if(itemOrder  !== this.allItems.length){
+                this.$store.commit('addPage')
+              }
+            } else {
+              // document.getElementById(this.ids).insertBefore('<p>111111111111</p>')
+              console.log('新的开始')
+              document.getElementById(this.ids).style.marginTop =  ((PAPER_H - SVG_BORDER_PROP.height - SVG_BORDER_PROP.y) + SVG_BORDER_PROP.y + 40) + "px";
+              this.$store.commit('changeNewPage',false)
+            }
+          } else {
+            this.$store.commit('changeNewPage',false)
+          }
+          this.$root.$children[0].spinShow = false
+
+          // this.myEditor.txt.append('curEditorY:' + curEditorY + 'px');
+          // this.myEditor.txt.append('height:' + this.myEditor.config.height + 'px');
+
+        }, 30);
+
+        this.myEditor.txt.append(addStr + brHtml);
+      });
+    },
+
+    define({ editor, prop, change, validate }) {
+      if (typeof validate != "function") {
+        validate = function (value) {
+          return {
+            valid: true, // 验证的结果。true: 验证通过;false: 验证失败。
+            data: value, // 处理后的值,不需要处理直接返回即可
+          };
+        };
+      }
+      let temp = editor.config[prop];
+      Object.defineProperty(editor.config, prop, {
+        enumerable: true,
+        configurable: true,
+        get: function () {
+          return temp;
+        },
+        set: function (value) {
+          const { valid, data } = validate(value);
+          if (valid && data !== temp) {
+            temp = data;
+            change(temp);
+          }
+          return temp;
+        },
+      });
+    },
+
+    // 拖拽改变编辑器的高度
+    dragReSize(editor) {
+      // 将一个 HTML 字符串转换为由 wangEditor.$(一个类 jQuery 的东东) 包裹的实例对象
+      const reel = E.$(
+        '<div style="position: absolute;bottom: 0;left: 0;width: 100%;background-color: none;cursor: n-resize;border-top: 3px solid transparent;"></div>'
+      );
+      // 将这个实例添加到 wangEditor 的编辑区容器中
+      editor.$textContainerElem.append(reel); // 添加鼠标事件
+      reel.on("mousedown", function mousedown(down) {
+        let last = down; // 鼠标移动,更新 wangEditor 实例中的 height 配置项
+        function mousemove(move) {
+          editor.config.height += move.clientY - last.clientY;
+          last = move;
+        } // 在鼠标弹起事件中移除事件监听
+        function mouseup() {
+          document.removeEventListener("mousemove", mousemove);
+          document.removeEventListener("mouseup", mouseup);
+        }
+        // 将鼠标事件绑定到 document 上
+        document.addEventListener("mousemove", mousemove);
+        document.addEventListener("mouseup", mouseup);
+      });
+    },
+
+    equalAltitude({ editor, max, min }) {
+      editor.config.onchange = function () {
+        const last = Array.prototype.slice.call(
+          editor.$textElem.elems[0].children,
+          -1
+        )[0];
+        if (last) {
+          const height = last.offsetTop + last.clientHeight;
+          // editor.config.height = editor.config.height = height < min ? min : height > max ? max : height
+        }
+      };
+    },
+
+    doRenderContent(content){
+      this.myEditor.txt.clear()
+      this.myEditor.txt.append(content);
+    }
+  },
+  mounted() {
+    const editor = new E("#" + this.ids);
+    editor.config.height = this.type === "0" ? "" : 40;
+
+    this.define({
+      editor: editor,
+      prop: "height",
+      change(value) {
+        editor.$textContainerElem.css("height", `${value}px`);
+      },
+      validate(value) {
+        const data = parseFloat(value);
+        return {
+          valid: !isNaN(data), // 数据校验:设置的值只能为数字
+          data: data, // 此处返回的是被处理后的数据
+        };
+      },
+    });
+    // 因为动态更新编辑器高度涉及到 wangEditor 的 onchange 回调,
+    // 而 onchange 必须在 create 前的配置才有效,所以放在 create 前。
+    this.equalAltitude({ editor: editor });
+
+    editor.config.menus = [
+      "bold",
+      "fontSize",
+      "fontName",
+      "italic",
+      "underline",
+      "strikeThrough",
+      "indent",
+      "lineHeight",
+      "foreColor",
+      "backColor",
+      "undo",
+    ];
+
+    editor.config.onfocus = (html) => {
+      let toolbarDom = document.getElementById(this.ids).getElementsByClassName("w-e-toolbar")[0]
+      let editorDom = document.getElementById(this.ids).getElementsByClassName("w-e-text")[0]
+      editorDom.style.border = '1px solid #000'  
+      toolbarDom.style.display = "flex";
+    };
+
+    editor.config.onblur = (html) => {
+      let toolbarDom = document.getElementById(this.ids).getElementsByClassName("w-e-toolbar")[0]
+      let editorDom = document.getElementById(this.ids).getElementsByClassName("w-e-text")[0]
+      editorDom.style.border = '0 solid #000'  
+      toolbarDom.style.display = "none";
+    };
+    editor.config.focus = false;
+    editor.config.showFullScreen = false;
+    editor.config.placeholder = "";
+    editor.create();
+    this.dragReSize(editor);
+    this.myEditor = editor;
+  },
+  watch: {
+    content: {
+      handler(n, o) {
+        this.$nextTick(() => {
+          this.doRenderContent('<p style="font-weight:bold;color:#000">' + n + '</p>');
+        })
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style>
+.sheet-title-Editor {
+  position: relative;
+  width: 590px;
+  margin: 2px auto;
+  border: 0 solid #000 !important;
+}
+
+.sheet-title-Editor .w-e-toolbar {
+  display: none;
+  position: absolute;
+  top: -32px;
+  left: -1px;
+  width: 590px;
+  border: 1px solid #000 !important;
+  border-bottom: none !important;
+}
+
+.sheet-title-Editor .w-e-text-container {
+  border: none !important;
+  z-index: 1 !important;
+}
+
+.sheet-title-Editor .w-e-text {
+  font-size: 12px;
+  padding: 0;
+}
+
+.sheet-title-Editor .w-e-toolbar .w-e-menu {
+  width: 30px;
+  height: 30px;
+}
+
+.sheet-title-Editor .underline {
+  /* text-decoration: underline; */
+  border-bottom: 1px solid #333;
+  margin: 10px;
+  display: inline-block;
+}
+
+.sheet-title-Editor .complete-item {
+  margin: 0 4px;
+  display: inline-block;
+}
+</style>

+ 262 - 0
TEAMModelOS/ClientApp/src/view/answersheet/SheetBaseInfo.vue

@@ -0,0 +1,262 @@
+<template>
+  <div
+    class="sheet-info-container"
+    ref="baseInfoRef"
+    @mouseout="isShowInfoEdit = false"
+    @mouseover="isShowInfoEdit = true"
+  >
+    <svg id="sheetInfoSvg" width="100%" height="100%"></svg>
+    <div class="info-edit" v-show="isShowInfoEdit" @click="editModal = true">
+      <span>编辑</span>
+    </div>
+    <Modal v-model="editModal" title="配置基本信息项" @on-ok="onEditBaseInfo">
+      <p class="edit-title">需要填写内容</p>
+      <CheckboxGroup v-model="showInfoList">
+        <Checkbox
+          v-for="(item, index) in infoList"
+          :key="index"
+          :label="item"
+          :disabled="index < 2"
+        ></Checkbox>
+      </CheckboxGroup>
+      <p class="edit-title">准考证号位数</p>
+      <InputNumber :max="10" :min="4" v-model="idLength"></InputNumber>
+    </Modal>
+  </div>
+</template>
+ 
+<script>
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import {
+  PAPER_W,
+  PAPER_H,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "@/utils/sheetConfig.js";
+export default {
+  components: {},
+  data() {
+    return {
+      infoList: ["考号", "姓名", "学校", "班级", "座号", "考场"],
+      showInfoList: ["考号", "姓名", "学校", "班级", "座号"],
+      isShowInfoEdit: false,
+      editModal: false,
+      infoLeftBox: null,
+      idNumberBox: null,
+      snap: null,
+      idLength: 10,
+    };
+  },
+  created() {
+    this.snap = Snap("#sheetInfoSvg");
+  },
+  methods: {
+    onEditBaseInfo() {
+      this.infoLeftBox.remove();
+      let snap = this.snap;
+      this.infoLeftBox = snap.paper.g();
+      let gap = INFO_H / (this.showInfoList.length + 1);
+      this.showInfoList.forEach((item, index) => {
+        // 考号、座号、姓名等
+        let leftInfo1 = snap.text(
+          CONTENT_START_X + 30,
+          CONTENT_START_Y + INFO_ITEM_MARGIN + gap * index,
+          `${item} :`
+        );
+        // 下划线
+        let leftInfo2 = snap
+          .line(
+            CONTENT_START_X + 80,
+            CONTENT_START_Y + INFO_ITEM_MARGIN + gap * index,
+            CONTENT_START_X + 250,
+            CONTENT_START_Y + INFO_ITEM_MARGIN + gap * index
+          )
+          .attr({ fill: "none", stroke: "#000", strokeWidth: 1 });
+        this.infoLeftBox.add(leftInfo1, leftInfo2);
+      });
+
+      this.renderIdNumber(this.idLength);
+    },
+
+    // 渲染准考证号填涂区域
+    renderIdNumber(idLength) {
+      let snap = this.snap;
+      this.idNumberBox.remove();
+      this.idNumberBox = snap.paper.g();
+
+      const INFO_LEFT_W = 275; // 左侧的宽度
+      const INFO_LEFT_X =
+        INFO_LEFT_W + CONTENT_ML + ANCHORPROP.width + ANCHORPROP.gapX; // 左侧的x坐标
+
+      var numbers = new Array(idLength).fill("0");
+      var cellWidth = (INFO_W - INFO_LEFT_W) / idLength; // 计算每个号码的宽度
+      const NUMBER_ITEM_MLR =
+        ((INFO_W - INFO_LEFT_W) / idLength - NUMBER_ITEM_W) / 2; // 计算号码左右间距
+
+      // 信息框左右分割线
+      this.idNumberBox.add(
+        snap
+          .line(
+            INFO_LEFT_X,
+            CONTENT_MT + 50,
+            INFO_LEFT_X,
+            CONTENT_MT + 50 + INFO_H
+          )
+          .attr({ fill: "none", stroke: "#000", strokeWidth: 1 })
+      );
+
+      // 准考证号下边框
+      this.idNumberBox.add(
+        snap
+          .line(
+            INFO_LEFT_X,
+            ID_TITLE_Y,
+            PAPER_W - CONTENT_ML - ANCHORPROP.width - ANCHORPROP.gapX,
+            ID_TITLE_Y
+          )
+          .attr({ fill: "none", stroke: "#000", strokeWidth: 1 })
+      );
+      // 准考证号
+      this.idNumberBox.add(
+        snap.text(
+          INFO_LEFT_X + (INFO_LEFT_W - 20) / 2,
+          CONTENT_MT + 75,
+          "准 考 证 号"
+        )
+      );
+
+      this.idNumberBox.add(
+        // 填涂上边框
+        snap
+          .line(
+            INFO_LEFT_X,
+            ID_TITLE_Y + NUMBER_CELL_H,
+            PAPER_W - CONTENT_ML - ANCHORPROP.width - ANCHORPROP.gapX,
+            ID_TITLE_Y + NUMBER_CELL_H
+          )
+          .attr({ fill: "none", stroke: "#000", strokeWidth: 1 })
+      );
+
+      numbers.forEach((item, index) => {
+        // 画准考证号填涂对应锚点
+        let c1 = snap.rect(
+          INFO_LEFT_X + NUMBER_ITEM_MLR + index * cellWidth,
+          40,
+          NUMBER_ITEM_W,
+          NUMBER_ITEM_H
+        );
+
+        //  画准考证号分列分割线
+        let c2 = snap
+          .line(
+            INFO_LEFT_X + cellWidth * index,
+            ID_TITLE_Y,
+            INFO_LEFT_X + cellWidth * index,
+            ID_TITLE_Y - ID_TITLE_H + INFO_H
+          )
+          .attr({
+            fill: "none",
+            stroke: "#000",
+            strokeWidth: index === 0 ? 0 : 1,
+          });
+
+        this.idNumberBox.add(c1, c2);
+
+        // 画填涂svg图片
+        for (let i = 0; i < 10; i++) {
+          let img = require("@/icons/answersheet/" + i + ".svg");
+          let c3 = snap.image(
+            img,
+            INFO_LEFT_X + NUMBER_ITEM_MLR + cellWidth * index,
+            ID_TITLE_Y + NUMBER_CELL_H + 5 + NUMBER_ITEM_MT * i,
+            NUMBER_ITEM_W,
+            NUMBER_ITEM_H
+          );
+          this.idNumberBox.add(c3);
+        }
+      });
+    },
+  },
+  mounted: function () {
+    this.snap = Snap("#sheetInfoSvg");
+    var snap = this.snap;
+    // 定义分组Group
+    this.infoLeftBox = snap.paper.g();
+    this.idNumberBox = snap.paper.g();
+    // 渲染个人信息区域
+    this.onEditBaseInfo();
+    // 画左边准考证号定位锚点
+    for (let i = 0; i < 10; i++) {
+      snap.rect(
+        20,
+        ID_TITLE_Y + NUMBER_CELL_H + 5 + NUMBER_ITEM_MT * i,
+        NUMBER_ITEM_W,
+        NUMBER_ITEM_H
+      );
+    }
+    // 信息框
+    let infoBox = snap
+      .rect(CONTENT_START_X, CONTENT_START_Y, INFO_W, INFO_H)
+      .attr({ fill: "rgba(0,0,0,0)", stroke: "#000", strokeWidth: 1 });
+    infoBox.mouseover(() => {
+      // 移入
+      this.isShowInfoEdit = true;
+    });
+    infoBox.mouseout(() => {
+      // 移入
+      this.isShowInfoEdit = false;
+    });
+  },
+};
+</script>
+<style scoped>
+.sheet-info-container {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 750px;
+  height: 340px;
+}
+
+/* .sheet-info-container:hover .info-edit{
+    display: flex;
+} */
+
+.info-edit {
+  position: absolute;
+  top: 125px;
+  right: 110px;
+  width: 40px;
+  height: 25px;
+  background: #00a43a;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  font-size: 12px;
+  cursor: pointer;
+  z-index: 0;
+}
+
+.edit-title {
+  margin: 10px 0;
+}
+</style>
+ 

+ 106 - 0
TEAMModelOS/ClientApp/src/view/answersheet/SheetComplete.vue

@@ -0,0 +1,106 @@
+<template>
+  <div
+    class="sheet-complete-container"
+    ref="baseInfoRef"
+    @mouseout="isShowInfoEdit = false"
+    @mouseover="isShowInfoEdit = true"
+  >
+    <!-- <p style="margin-left: 100px; font-weight: bold; color: #000">二、填空题(共{{ completeItems.length }}题,总计 {{ totalScore }} 分)</p> -->
+    <BaseTitleEditor ids="completeTitleEditor" :content="'二、填空题(共' + completeItems.length + '题,总计' + totalScore + '分)'"></BaseTitleEditor>
+    <BaseEditor ids="completeEditor" :items="items" type="0"></BaseEditor>
+  </div>
+</template>
+ 
+<script>
+import BaseTitleEditor from './BaseTitleEditor'
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import BaseEditor from './BaseEditor'
+import {
+  PAPER_W,
+  PAPER_H,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  },
+  components:{ BaseEditor,BaseTitleEditor },
+  data() {
+    return {
+      isShowInfoEdit: false,
+      editModal: false,
+      completeGroup: null,
+      snap: null,
+      svgHeight: 0,
+      number: 5,
+      completeItems: [],
+    };
+  },
+  created() {
+    // this.snap = Snap("#sheetCompleteSvg");
+  },
+  methods: {
+  },
+  mounted: function () {
+    // this.snap = Snap("#sheetCompleteSvg");
+    // this.completeGroup = this.snap.paper.g();
+  },
+  computed:{
+    totalScore(){
+      console.log(this.completeItems)
+      return this.completeItems.length ? this.completeItems.map(i => i.score).reduce((a,b) => a + b) : 0
+    }
+  },
+  watch: {
+    items: {
+      // 数据变化时执行的逻辑代码
+      handler(n, old) {
+        if (n.length) {
+          let objectiveArr = ["complete"];
+          this.completeItems = n.filter((item) =>
+            objectiveArr.includes(item.type)
+          );
+        }
+      },
+      immediate:true,
+      // 开启深度监听
+      deep: true,
+    },
+  },
+};
+</script>
+<style scoped>
+.sheet-complete-container {
+  position: relative;
+  width: 750px;
+}
+
+
+.edit-title {
+  margin: 10px 0;
+}
+</style>
+ 

+ 340 - 0
TEAMModelOS/ClientApp/src/view/answersheet/SheetObjective.vue

@@ -0,0 +1,340 @@
+<template>
+  <div
+    class="sheet-objective-container"
+    ref="baseInfoRef"
+    @mouseout="isShowInfoEdit = false"
+    @mouseover="isShowInfoEdit = true"
+  >
+    <!-- <p style="margin-left: 100px; font-weight: bold; color: #000">一、选择题</p> -->
+    <BaseTitleEditor ids="objectiveTitleEditor" :content="'一、选择题(共' + objectiveItems.length + '题,总计' + totalScore + '分)'"></BaseTitleEditor>
+    <svg
+      id="sheetObjectiveSvg"
+      width="100%"
+      :style="'height:' + svgHeight + 'px'"
+    ></svg>
+    <div class="info-edit" v-show="isShowInfoEdit" @click="editModal = true">
+      <span>编辑</span>
+    </div>
+    <Modal v-model="editModal" title="配置基本信息项" @on-ok="onReRender">
+      <p class="edit-title">每列显示题数</p>
+      <InputNumber
+        :max="objectiveItems.length"
+        :min="3"
+        v-model="number"
+      ></InputNumber>
+    </Modal>
+  </div>
+</template>
+ 
+<script>
+import BaseTitleEditor from './BaseTitleEditor'
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import BaseObjective from "./BaseObjective.vue";
+import {
+  PAPER_W,
+  PAPER_H,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+  BLOCK_H
+} from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  },
+  components: { BaseObjective,BaseTitleEditor },
+  data() {
+    return {
+      isShowInfoEdit: false,
+      editModal: false,
+      objectiveGroup: null,
+      snap: null,
+      svgHeight: 0,
+      number: 5,
+      blockHeight:0,
+      objectiveItems: [],
+      leftItems:[],
+      blockList:[],
+      anchorRectY:0
+    };
+  },
+  created() {
+    this.snap = Snap("#sheetObjectiveSvg");
+  },
+  methods: {
+    // 修改每列展示题数重新渲染
+    onReRender() {
+      this.leftItems = JSON.parse(JSON.stringify(this.objectiveItems))
+      this.doRenderItems(this.objectiveItems, this.number);
+    },
+
+    // 渲染客观题区域边框
+    doRenderBorder(items, number, blockCount) {
+      console.log(blockCount);
+      let snap = this.snap;
+      let objectiveHeight = 40 + this.blockHeight * blockCount;
+      console.log(this.blockHeight);
+      this.svgHeight = objectiveHeight + 40;
+      // 信息框
+      let infoBox = snap
+        .rect(CONTENT_START_X, 2, INFO_W, this.anchorRectY + 20)
+        .attr({ fill: "rgba(0,0,0,0)", stroke: "#000", strokeWidth: 1 });
+      this.objectiveGroup.add(infoBox);
+    },
+
+    // 渲染客观题目
+    doRenderItems(items, number) {
+      this.blockHeight = 26 * number
+      this.objectiveGroup.remove(); // 先清除之前的绘制内容
+      this.objectiveGroup = this.snap.paper.g();
+      let snap = this.snap;
+      let MARGIN_LEFT = 25;
+      let GAP = 22;
+      let optionGap = NUMBER_ITEM_W + 5;
+      let startY = GAP + 15;
+      
+      let needNewLineArr = items.map((item, index) =>
+        this.needNewLine(index, number, items)
+      );
+      let indexAndNeed = [];
+      // 提前计算 每个题目排列时 是否要进行换行 以及计算需要换到第几行 (行可以理解为选项块)
+      for (let jIndex = 0; jIndex < needNewLineArr.length; jIndex++) {
+        if (needNewLineArr[jIndex].need) {
+          indexAndNeed.push({
+            index: jIndex,
+            need: true,
+          });
+        }
+      }
+      let itemsPositionArr = [];
+      // 提前计算 每个题目排列时 是否要进行换行 以及计算需要换到第几行 (行可以理解为选项块)
+      for (let index = 0; index < items.length; index++) {
+        itemsPositionArr.push({
+          index: index,
+          needNewLine: needNewLineArr[index].need,
+          lineIndex: needNewLineArr[index].blockIndex
+        });
+      }
+      console.log(itemsPositionArr)
+      let blockList = [...new Set(itemsPositionArr.map(i => i.lineIndex))]
+      this.blockList = blockList
+      
+      items.forEach((item, index) => {
+        if(index > 0 && (itemsPositionArr[index].lineIndex !== itemsPositionArr[index - 1].lineIndex)){
+          this.leftItems = this.leftItems.slice(index,this.leftItems.length)
+        }
+        let optionX = CONTENT_START_X + MARGIN_LEFT + this.getStartX(number, this.leftItems,item.id);
+        let optionY =   startY + GAP * (index + 1 <= number ? index : index % number) + ((itemsPositionArr[index].lineIndex + 1) * this.blockHeight)
+        // console.log(index,optionX)
+        // 渲染选项下标
+        let itemOrder = snap
+          .text(
+            optionX,
+            optionY,
+            index + 1
+          )
+          .attr({
+            textAnchor: "end",
+            width: 40,
+            fontSize: 12,
+            fontFamily: "Times New Roman",
+          });
+        this.objectiveGroup.add(itemOrder);
+        item.option.forEach((option, optionIndex) => {
+          let code = String.fromCharCode(
+            64 + parseInt(optionIndex + 1)
+          ).toLowerCase();
+          // 渲染选项图片
+          // let img = require("../../icons/answersheet/" + code + ".svg");
+          let img = require('@/icons/answersheet/0.svg');
+          let c3 = snap.image(
+            img,
+            optionX + NUMBER_ITEM_ML + optionGap * optionIndex,
+            optionY - 10,
+            NUMBER_ITEM_W,
+            NUMBER_ITEM_H
+          );
+          this.objectiveGroup.add(c3);
+          
+        });
+      });
+
+      let diff = this.leftItems.length - this.number
+      let refHeight = this.blockHeight * (blockList.length - 1)
+      let objectiveHeight = diff > 0 ? refHeight : refHeight - 26 * diff * -1
+      this.anchorRectY = startY + GAP * number + objectiveHeight
+      for (let index = 0; index < 25; index++) {
+        // 渲染选项框横向定位锚点
+          this.objectiveGroup.add(
+            snap.rect(
+              CONTENT_START_X + MARGIN_LEFT + NUMBER_ITEM_ML + optionGap * index,
+              startY + GAP * number + objectiveHeight,
+              NUMBER_ITEM_W,
+              NUMBER_ITEM_H
+            )
+          );
+      }
+      // 添加纵向定位锚点
+      for (let index = 0; index < number; index++) {
+        this.objectiveGroup.add(
+          snap.rect(20, startY + GAP * index - 10, NUMBER_ITEM_W, NUMBER_ITEM_H)
+        );
+      }
+
+      // 渲染包围框
+      this.doRenderBorder(items, number, blockList.length);
+    },
+
+    // 获取每一列渲染的x坐标
+    getStartX(number, items,itemId) {
+      // 根据index求出当前列最多选项的题目从而获取当前列的宽度
+      let columnCount = Math.ceil(items.length / number); // 总共多少列
+      let curColumn = parseInt(items.map(i => i.id).indexOf(itemId) / number); // 当前index在第几列
+      let columnWidthArr = []; // 存放每一列的距前一列的宽度
+      let singleOptionW = NUMBER_ITEM_W + NUMBER_ITEM_ML;
+      let columnGap = 10 // 列与列之间的间隔
+      let orderWidth = 11 // 题号的最大宽度
+      for (var i = 0; i < columnCount; i++) {
+        if (i === 0) {
+          columnWidthArr.push(0); // 第一列左间距为0
+        } else {
+          // 后面每一列的取值为前一列最多选项宽度
+          let startIndex = number * (i - 1);
+          let maxOpitonCount = items
+            .slice(startIndex, startIndex + number)
+            .map((i) => i.option.length);
+          columnWidthArr.push(singleOptionW * Math.max(...maxOpitonCount) + orderWidth + columnGap);
+        }
+      }
+      // 第几列就取前几列的宽度总和 作为x值
+      return columnWidthArr.slice(0, curColumn + 1).length ? columnWidthArr.slice(0, curColumn + 1).reduce((a, b) => a + b) : 0;
+    },
+
+    needNewLine(index, number, items) {
+      let columnCount = Math.ceil(items.length / number); // 总共多少列
+      let curColumn = parseInt(index / number); // 当前index在第几列
+      let preColumnCells = [];
+      let columnCells = [];
+      let cellCountMax = 20; // 每行最多能放的选项数
+      for (var i = 0; i < columnCount; i++) {
+        if (i > 0) {
+          // 后面每一列的取值为前一列最多选项宽度
+          let startIndex = number * (i - 1);
+          let maxOpitonCount = items
+            .slice(startIndex, startIndex + number)
+            .map((i) => i.option.length);
+          preColumnCells.push(Math.max(...maxOpitonCount));
+        }
+
+        // 去所有列的最大选项数 方便得知自己当前列的最大值
+        let startIndex2 = number * i;
+        let maxOpitonCount2 = items
+          .slice(startIndex2, startIndex2 + number)
+          .map((i) => i.option.length);
+        columnCells.push(Math.max(...maxOpitonCount2));
+      }
+      let cellCount = preColumnCells.slice(0, curColumn).length
+        ? preColumnCells.slice(0, curColumn).reduce((a, b) => a + b)
+        : 0; // 目前已经有的选项数
+      // 如果前面所有列的最大选项数之和 加上 当前列的最大选项数  大于最大值 则需要换行
+      let totalCell = cellCount + columnCells[curColumn]
+      return {
+        need:totalCell > cellCountMax, // 是否需要换行
+        blockIndex:totalCell > cellCountMax ? parseInt(totalCell / cellCountMax) - 1 : -1 // 当前需要换到第几行
+      };
+    },
+  },
+  mounted: function () {
+    this.snap = Snap("#sheetObjectiveSvg");
+    this.objectiveGroup = this.snap.paper.g();
+
+    // this.$nextTick(() => {
+    //   this.$emit("onRendered", this.$refs.baseInfoRef.clientHeight);
+    // });
+  },
+  computed:{
+    totalScore(){
+      return this.objectiveItems.length ? this.objectiveItems.map(i => i.score).reduce((a,b) => a + b) : 0
+    }
+  },
+  watch: {
+    items: {
+      // 数据变化时执行的逻辑代码
+      handler(n, old) {
+        if (n.length) {
+          let objectiveArr = ["single", "multiple", "judge"];
+          this.objectiveItems = n.filter((item) =>
+            objectiveArr.includes(item.type)
+          );
+          this.blockHeight = 26 * this.number
+          this.leftItems = JSON.parse(JSON.stringify(this.objectiveItems)) 
+          this.doRenderItems(this.objectiveItems, this.number);
+        }
+      },
+      // 开启深度监听
+      deep: true,
+    },
+    anchorRectY: {
+      handler(n, o) {
+        this.$nextTick(() => {
+          this.$emit("onRendered", n);
+        });
+      },
+    },
+  },
+};
+</script>
+<style scoped>
+.sheet-objective-container {
+  position: absolute;
+  left: 0;
+  top: 340px;
+  width: 750px;
+}
+
+/* .sheet-info-container:hover .info-edit{
+    display: flex;
+} */
+
+.info-edit {
+  position: absolute;
+  top: 55px;
+  right: 110px;
+  width: 40px;
+  height: 25px;
+  background: #00a43a;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  font-size: 12px;
+  cursor: pointer;
+  z-index: 0;
+}
+
+.edit-title {
+  margin: 10px 0;
+}
+</style>
+ 

+ 119 - 0
TEAMModelOS/ClientApp/src/view/answersheet/SheetSubjective.vue

@@ -0,0 +1,119 @@
+<template>
+  <div
+    class="sheet-subjective-container"
+    ref="baseInfoRef"
+    @mouseout="isShowInfoEdit = false"
+    @mouseover="isShowInfoEdit = true"
+  >
+    <!-- <p style="margin:10px 0 10px 100px; font-weight: bold; color: #000">三、问答题(共{{ subjectiveItems.length }}题,总计 {{  totalScore  }} 分)</p> -->
+    <BaseTitleEditor ids="subjectiveTitleEditor" :content="'三、问答题(共' + subjectiveItems.length + '题,总计' + totalScore + '分)'"></BaseTitleEditor>
+    <div v-if="isRender">
+      <BaseEditor
+        v-for="(item, index) in subjectiveItems"
+        :key="index"
+        :ids="'subjectiveEditor' + index"
+        :allItems="items"
+        :items="[item]"
+        type="1"
+      ></BaseEditor>
+    </div>
+  </div>
+</template>
+ 
+<script>
+import BaseTitleEditor from './BaseTitleEditor'
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import BaseEditor from "./BaseEditor";
+import {
+  PAPER_W,
+  PAPER_H,
+  CONTENT_MT,
+  CONTENT_ML,
+  ANCHORPROP,
+  CONTENT_START_X,
+  CONTENT_START_Y,
+  INFO_W,
+  INFO_H,
+  INFO_ITEM_GAP,
+  INFO_ITEM_MARGIN,
+  ID_LENGTH,
+  INFO_LEFT_X,
+  INFO_LEFT_W,
+  ID_TITLE_H,
+  ID_TITLE_Y,
+  NUMBER_CELL_H,
+  NUMBER_ITEM_W,
+  NUMBER_ITEM_H,
+  NUMBER_ITEM_MT,
+  NUMBER_ITEM_ML,
+} from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    items: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  },
+  components: { BaseEditor, BaseTitleEditor },
+  data() {
+    return {
+      isRender:true,
+      isShowInfoEdit: false,
+      editModal: false,
+      subjectiveGroup: null,
+      snap: null,
+      svgHeight: 0,
+      subjectiveItems: [],
+    };
+  },
+  created() {
+    this.$store.commit('clearFixArr')
+  },
+  methods: {},
+  mounted() {
+    this.$EventBus.$on("doRefresh", () => {
+      this.isRender = false
+      setTimeout(() => {
+        this.$store.commit('clearFixArr')
+        this.$store.commit('clearPage')
+        document.getElementById('pdfDom').scrollIntoView()
+        this.isRender = true
+      },)
+    });
+  },
+  computed:{
+    totalScore(){
+      console.log(this.subjectiveItems)
+      return this.subjectiveItems.length ? this.subjectiveItems.map(i => i.score).reduce((a,b) => a + b) : 0
+    }
+  },
+  watch: {
+    items: {
+      // 数据变化时执行的逻辑代码
+      handler(n, old) {
+        if (n.length) {
+          let objectiveArr = ["subjective"];
+          this.subjectiveItems = n.filter((item) =>
+            objectiveArr.includes(item.type)
+          );
+        }
+      },
+      // 开启深度监听
+      deep: true,
+    },
+  },
+};
+</script>
+<style scoped>
+.sheet-subjective-container {
+  position: relative;
+  width: 750px;
+}
+
+.edit-title {
+  margin: 10px 0;
+}
+</style>
+ 

+ 134 - 0
TEAMModelOS/ClientApp/src/view/answersheet/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="sheet-container" id="pdfDom" v-if="isRender">
+    <!-- <router-view  v-if="isRouterAlive"/> -->
+    <BaseSvgBg
+      v-for="(page, pageIndex) in pages"
+      :key="pageIndex"
+      :ids="'svg' + pageIndex"
+      :total="pages.length"
+    ></BaseSvgBg>
+    <SheetBaseInfo></SheetBaseInfo>
+    <SheetObjective
+      :items="items"
+      v-show="groupItems.objectiveItems.length"
+      @onRendered="onRendered"
+    ></SheetObjective>
+    <div class="sheet-subjective-wrap" :style="'top:' + subjectiveTop + 'px'">
+      <SheetComplete
+        :items="items"
+        v-if="groupItems.completeItems.length"
+      ></SheetComplete>
+      <SheetSubjective
+        :items="items"
+        v-show="groupItems.subjectiveItems.length"
+      ></SheetSubjective>
+    </div>
+  </div>
+</template>
+ 
+<script>
+import Snap from "imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js";
+import SheetObjective from "./SheetObjective";
+import SheetBaseInfo from "./SheetBaseInfo";
+import SheetComplete from "./SheetComplete";
+import SheetSubjective from "./SheetSubjective";
+import BaseSvgBg from "./BaseSvgBg";
+import { PAPER_W, PAPER_H, ANCHORPROP, INFO_H } from "@/utils/sheetConfig.js";
+export default {
+  props: {
+    ids: {
+      type: String,
+      default: "",
+    },
+  },
+  provide :function() {
+    return {
+      reload:this.reload
+    }
+},
+  components: {
+    SheetObjective,
+    SheetBaseInfo,
+    SheetComplete,
+    SheetSubjective,
+    BaseSvgBg,
+  },
+  data() {
+    return {
+      isRouterAlive:true,
+      isRender: true,
+      pages: [],
+      items: [],
+      groupItems: {
+        objectiveItems: [],
+        completeItems: [],
+        subjectiveItems: [],
+      },
+      subjectiveTop: 0,
+    };
+  },
+  methods: {
+    reload(){
+        this.isRouterAlive=false;
+        this.$nextTick(function(){
+            this.isRouterAlive=true
+        })
+    },
+    // 客观题渲染完 来决定主观题的TOP值高度
+    onRendered(val) {
+      this.subjectiveTop = val + INFO_H + 220;
+    },
+
+    doRender() {
+      this.groupItems = {
+        objectiveItems: [],
+        completeItems: [],
+        subjectiveItems: [],
+      }
+      this.items = require("@/utils/items.json");
+      let objectiveTypes = ["single", "multiple", "judge"];
+      this.items.forEach((i) => {
+        if (objectiveTypes.includes(i.type)) {
+          this.groupItems.objectiveItems.push(i);
+        } else if (i.type === "complete") {
+          this.groupItems.completeItems.push(i);
+        } else {
+          this.groupItems.subjectiveItems.push(i);
+        }
+      });
+    },
+  },
+  mounted: function () {
+    this.doRender()
+  },
+  computed: {
+    getPage() {
+      return this.$store.state.answerSheet.pages;
+    },
+  },
+  watch: {
+    getPage: {
+      handler(n, o) {
+        this.pages = new Array(n).fill(0);
+      },
+    },
+  },
+};
+</script>
+<style scoped>
+.sheet-container {
+  position: relative;
+  width: 750px;
+  height: 1060.71px;
+  background: #fff;
+  /* margin: 0 auto; */
+}
+
+.sheet-subjective-wrap {
+  position: absolute;
+  top: 580px;
+  left: 0;
+  width: 100%;
+}
+</style>
+ 

+ 7 - 0
TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.less

@@ -217,6 +217,13 @@
 			}
 		}
 	}
+	
+	.pl-answersheet-wrap{
+		width: 100%;
+		display: flex;
+		justify-content: center;
+		background-color: #fff;
+	}
 }
 
 .no-data-text {

+ 33 - 4
TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue

@@ -34,6 +34,10 @@
 							<Icon type="md-download" />
 							<span>下载试卷</span>
 						</span> -->
+						<!-- <span class="paper-item-tools-edit" @click.stop="renderAnswerSheet(paper)">
+							<Icon type="md-download" />
+							<span>预览答题卡</span>
+						</span> -->
 						<span class="paper-item-tools-edit" @click.stop="goToPaper(paper)"
 							v-if="($access.can('admin.*||exercise-upd') || !isSchool)">
 							<Icon type="ios-create" />
@@ -51,10 +55,11 @@
 				<Page :total="totalNum" show-sizer show-total :page-size="pageSize" :current="pageNum"
 					@on-page-size-change="pageSizeChange" @on-change="pageChange" :page-size-opts="[10,20,30,40]" />
 			</div>
-
+			
+			<!-- 预览试卷部分 -->
 			<div class="pl-review-wrap animated fadeIn" v-if="isPreview">
 				<div class="pl-review-wrap-left">
-					<TestPaper :paper="evaluationInfo" isExamPaper isHideAnalysis></TestPaper>
+					<TestPaper :paper="evaluationInfo" isExamPaper></TestPaper>
 				</div>
 
 				<div class="pl-review-wrap-right">
@@ -68,12 +73,23 @@
 					<BasePointPie :echartsData="evaluationInfo"></BasePointPie>
 				</div>
 			</div>
+			
+			<!-- 预览答题卡部分 -->
+			<!-- <div class="pl-answersheet-wrap animated fadeIn" v-if="isShowSheet">
+				<AnswerSheet></AnswerSheet>
+			</div> -->
 		</div>
 
 
-		<Modal v-model="isShowDownLoad" width="800px"  title="下载试卷">
+		<Modal v-model="isShowDownLoad" width="800px"  title="下载试卷" class-name="preview-modal">
 			<DownloadPaper :paper="fullPaperJson"></DownloadPaper>
 		</Modal>
+		
+		<Modal v-model="isShowSheet" width="800px"  title="答题卡" class-name="preview-modal">
+			<div v-if="isShowSheet">
+				<AnswerSheet></AnswerSheet>
+			</div>
+		</Modal>
 
 	</div>
 </template>
@@ -82,15 +98,18 @@
 	import BaseFilter from '../components/BaseFilter'
 	import BaseImport from '../components/BaseImport'
 	import TestPaper from '../index/TestPaper.vue'
+	import AnswerSheet from '../../answersheet/index.vue'
 	export default {
 		components: {
 			Loading,
 			BaseFilter,
 			BaseImport,
-			TestPaper
+			TestPaper,
+			AnswerSheet
 		},
 		data() {
 			return {
+				isShowSheet:false,
 				containerClient: null,
 				schoolCode: '',
 				totalNum: 0,
@@ -256,6 +275,10 @@
 				this.isShowDownLoad = true
 				console.log('要下载的试卷', fullPaperJson)
 			},
+			
+			renderAnswerSheet(paper){
+				this.isShowSheet = true
+			},
 
 			/**
 			 * 删除试卷
@@ -399,6 +422,12 @@
 </style>
 
 <style>
+	
+	.preview-modal .ivu-modal-body{
+		height: 650px;
+		overflow: scroll;
+	}
+	
 	.pl-content-wrap .ivu-page {
 		display: flex;
 		flex-direction: row;

+ 0 - 1
TEAMModelOS/ClientApp/src/view/evaluation/components/BasePointPie.vue

@@ -26,7 +26,6 @@
 			   	    }
 			    	colorarr[i] = color;
 			    }
-				console.log(colorarr)
 			    return colorarr;
 			},
 			

+ 1 - 1
TEAMModelOS/ClientApp/src/view/evaluation/index/TestPaper.vue

@@ -38,7 +38,7 @@
 						<p class="paper-title">{{paperInfo.name}}</p>
 					</div>
 
-					<ExamPaperAnalysis :testPaper="paperInfo" v-if="isShowAnalysis"></ExamPaperAnalysis>
+					<ExamPaperAnalysis :testPaper="paperInfo" v-if="isShowAnalysis" :hidePie="isExamPaper"></ExamPaperAnalysis>
 
 					<!-- 题目类型及列表 -->
 					<BaseExerciseList :paper="paperInfo" @dataUpdate="onListUpdate" v-show="!isShowAnalysis" ref="exList" :isShowTools="!isPreview" :isExamPaper="isExamPaper"

+ 225 - 46
TEAMModelOS/ClientApp/src/view/learnactivity/ExamPaperAnalysis.vue

@@ -1,55 +1,234 @@
 <template>
-    <div class="exam-analysis-wrap">
-		<div class="exam-analysis-echarts-item">
-			<BaseTypePie :echartsData="testPaper"></BaseTypePie>
+	<div>
+		<div class="exam-analysis-checklist">
+			<p style="margin: 10px 0 20px 0;font-weight: bold;display: flex;justify-content: space-between;"><span>试卷双向细目分析表</span> <span style="color: #919185;">提示: a(b) a-配分,b-数量</span> </p>
+			<Table :columns="columns" :data="items"></Table>
 		</div>
-		<div class="exam-analysis-echarts-item">
-			<BaseObjectivePie :echartsData="testPaper"></BaseObjectivePie>
+		<div class="exam-analysis-wrap" v-if="!hidePie">
+			<div class="exam-analysis-echarts-item">
+				<BaseTypePie :echartsData="testPaper"></BaseTypePie>
+			</div>
+			<div class="exam-analysis-echarts-item">
+				<BaseObjectivePie :echartsData="testPaper"></BaseObjectivePie>
+			</div>
+			<div class="exam-analysis-echarts-item">
+				<BaseDiffPie :echartsData="testPaper"></BaseDiffPie>
+			</div>
+			<div class="exam-analysis-echarts-item">
+				<BasePointPie :echartsData="testPaper"></BasePointPie>
+			</div>
 		</div>
-		<div class="exam-analysis-echarts-item">
-			<BaseDiffPie :echartsData="testPaper"></BaseDiffPie>
-		</div>
-		<div class="exam-analysis-echarts-item">
-			<BasePointPie :echartsData="testPaper"></BasePointPie>
-		</div>
-    </div>
+	</div>
 </template>
 <script>
-    export default {
-        props: {
-            testPaper: {
-                type: Object,
-                default: () => {
-                    return {
-                        item: []
-                    }
-                }
-            }
-        },
-        data() {
-            return {
-                
-            }
-        },
-        methods: {
-            
-        },
-        mounted() {
-            
-        },
-        watch: {
-            testPaper: {
-                handler(n, o) {
-                },
-                deep: true
-            }
-        },
-        create() {
+	export default {
+		props: {
+			testPaper: {
+				type: Object,
+				default: () => {
+					return {
+						item: []
+					}
+				}
+			},
+			hidePie:{
+				type:Boolean,
+				default:false
+			}
+		},
+		data() {
+			return {
+				pointList:[],
+				columns: [{
+						title: '',
+						key: 'name'
+					},
+					{
+						title: '记忆',
+						key: 'level1',
+						render:(h,params) => {
+							return h('span', params.row.level1.score + '(' + params.row.level1.count  + ')')
+						}
+					},
+					{
+						title: '理解',
+						key: 'level2',
+						render:(h,params) => {
+							return h('span', params.row.level2.score + '(' + params.row.level2.count  + ')')
+						}
+					},
+					{
+						title: '应用',
+						key: 'level3',
+						render:(h,params) => {
+							return h('span', params.row.level3.score + '(' + params.row.level3.count  + ')')
+						}
+					},
+					{
+						title: '分析',
+						key: 'level4',
+						render:(h,params) => {
+							return h('span', params.row.level4.score + '(' + params.row.level4.count  + ')')
+						}
+					},
+					{
+						title: '评价',
+						key: 'level5',
+						render:(h,params) => {
+							return h('span', params.row.level5.score + '(' + params.row.level5.count  + ')')
+						}
+					},
+					{
+						title: '创造',
+						key: 'level6',
+						render:(h,params) => {
+							return h('span', params.row.level6.score + '(' + params.row.level6.count  + ')')
+						}
+					},
+					{
+						title: '合计',
+						key: 'level6',
+						render:(h,params) => {
+							return h('span', this.sumScore(params.row) + '(' + this.sumCount(params.row)  + ')')
+						}
+					}
+				],
+				items: []
+			}
+		},
+		methods: {
+			sumScore(row){
+				return row.level1.score + row.level2.score + row.level3.score + row.level4.score + row.level5.score + row.level6.score
+			},
+			sumCount(row){
+				return row.level1.count + row.level2.count + row.level3.count + row.level4.count + row.level5.count + row.level6.count
+			}
+		},
+		mounted() {
+			let items = []
+			this.testPaper.item.forEach(i => {
+				if(i.type !== 'compose'){
+					items.push(i)
+				}else{
+					items.push(...i.children)
+				}
+			})
+			if(items.length){
+				let pointList = [...new Set(items.map(i => i.knowledge).flat(1))]
+				let result = []
+				result.push({
+					name:'无',
+					level1:{
+						score:0,
+						count:0
+					},
+					level2:{
+						score:0,
+						count:0
+					},
+					level3:{
+						score:0,
+						count:0
+					},
+					level4:{
+						score:0,
+						count:0
+					},
+					level5:{
+						score:0,
+						count:0
+					},
+					level6:{
+						score:0,
+						count:0
+					},
+				})
+				pointList.forEach(i => {
+					result.push({
+						name:i,
+						level1:{
+							score:0,
+							count:0
+						},
+						level2:{
+							score:0,
+							count:0
+						},
+						level3:{
+							score:0,
+							count:0
+						},
+						level4:{
+							score:0,
+							count:0
+						},
+						level5:{
+							score:0,
+							count:0
+						},
+						level6:{
+							score:0,
+							count:0
+						},
+					})
+				})
+				if(pointList.length){
+					pointList.forEach((point,pointIndex) => {
+						items.forEach(item => {
+							if(item.knowledge.includes(point)){
+								result[pointIndex + 1]['level'+ item.field].score += item.score
+								result[pointIndex + 1]['level'+ item.field].count++
+							}
+						})
+					})
+				}
+				items.forEach(item => {
+					if(!item.knowledge.length){
+						result[0]['level'+ item.field].score += item.score
+						result[0]['level'+ item.field].count++
+					} 
+				})
+				result.push({
+					name:'合计',
+					level1:{
+						score:result.reduce((a,b) => a + b.level1.score,0),
+						count:result.reduce((a,b) => a + b.level1.count,0)
+					},
+					level2:{
+						score:result.reduce((a,b) => a + b.level2.score,0),
+						count:result.reduce((a,b) => a + b.level2.count,0)
+					},
+					level3:{
+						score:result.reduce((a,b) => a + b.level3.score,0),
+						count:result.reduce((a,b) => a + b.level3.count,0)
+					},
+					level4:{
+						score:result.reduce((a,b) => a + b.level4.score,0),
+						count:result.reduce((a,b) => a + b.level4.count,0)
+					},
+					level5:{
+						score:result.reduce((a,b) => a + b.level5.score,0),
+						count:result.reduce((a,b) => a + b.level5.count,0)
+					},
+					level6:{
+						score:result.reduce((a,b) => a + b.level6.score,0),
+						count:result.reduce((a,b) => a + b.level6.count,0)
+					}
+				})
+				this.items = result
+			}
+		},
+		watch: {
+			testPaper: {
+				handler(n, o) {},
+				deep: true
+			}
+		},
+		create() {
 
-        }
-    }
+		}
+	}
 </script>
 <style lang="less" scoped>
-    @import './ExamPaperAnalysis.less';
+	@import './ExamPaperAnalysis.less';
 </style>
-

+ 69 - 59
TEAMModelOS/ClientApp/vue.config.js

@@ -1,66 +1,76 @@
 const path = require('path')
 const Timestamp = new Date().getTime();
+
 function resolve(dir) {
-    return path.join(__dirname, './', dir)
+	return path.join(__dirname, './', dir)
 }
 
 module.exports = {
-    outputDir: '../wwwroot',
-    //lintOnSave: process.env.NODE_ENV !== 'production',
-    lintOnSave: false,
-    pages: {
-        app: {
-            entry: 'src/main.js',
-            template: 'public/index.html',
-            filename: 'index.html',
-            excludeChunks: ['silent-renew-oidc']
-        },
-        silentrenewoidc: {
-            entry: 'src/silent-renew-oidc.js',
-            template: 'public/silent-renew-oidc.html',
-            filename: 'silent-renew-oidc.html',
-            excludeChunks: ['app']
-        }
-    },
-    chainWebpack(config) {
-        config.module
-            .rule('svg')
-            .exclude
-            .add(resolve('src/icons'))
-            .add(resolve('src/assets/student-web/icons'))
-            .end()
-        config.module
-            .rule('icons')
-            .test(/\.svg$/)
-            .include
-            .add(resolve('src/icons'))
-            .add(resolve('src/assets/student-web/icons'))
-            .end()
-            .use('svg-sprite-loader')
-            .loader('svg-sprite-loader')
-            .options({
-                symbolId: 'icon-[name]'
-            })
-            .end()
-    },
-    pluginOptions: {
-        'style-resources-loader': {
-            preProcessor: 'less',
-            patterns: [
-                // 这个是加上自己的路径,不能使用(如下:alias)中配置的别名路径
-                path.resolve(__dirname, './src/css/less-variable.less')
-            ]
-        }
-    },
-    configureWebpack: config => {
-        config.optimization.minimizer[0].options.terserOptions.compress.drop_console = process.env.NODE_ENV === 'production'
-        config.entry.app = ["babel-polyfill", "./src/main.js"];
-    },
-    configureWebpack: {
-        output: { // 输出重构  打包编译后的 文件名称  【模块名称.版本号.js】
-            filename: `js/[name].${Timestamp}.js`,
-            chunkFilename: `js/[name].${Timestamp}.js`
-            // chunkFilename: `js/[id].vw.js`
-        }
-    }
+	outputDir: '../wwwroot',
+	//lintOnSave: process.env.NODE_ENV !== 'production',
+	lintOnSave: false,
+	pages: {
+		app: {
+			entry: 'src/main.js',
+			template: 'public/index.html',
+			filename: 'index.html',
+			excludeChunks: ['silent-renew-oidc']
+		},
+		silentrenewoidc: {
+			entry: 'src/silent-renew-oidc.js',
+			template: 'public/silent-renew-oidc.html',
+			filename: 'silent-renew-oidc.html',
+			excludeChunks: ['app']
+		}
+	},
+	chainWebpack(config) {
+		config.module
+			.rule('svg')
+			.exclude
+			.add(resolve('src/icons'))
+			.add(resolve('src/assets/student-web/icons'))
+			.end()
+		config.module
+			.rule('icons')
+			.test(/\.svg$/)
+			.include
+			.add(resolve('src/icons'))
+			.add(resolve('src/assets/student-web/icons'))
+			.end()
+			.use('svg-sprite-loader')
+			.loader('svg-sprite-loader')
+			.options({
+				symbolId: 'icon-[name]'
+			})
+			.end()
+		// config.module
+		// 	.rule('images')
+		// 	.test(/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/)
+		// 	.use('url-loader')
+		// 	.loader('url-loader')
+		// 	.tap(options => Object.assign(options, {
+		// 		esModule: false
+		// 	}));
+	},
+	pluginOptions: {
+		'style-resources-loader': {
+			preProcessor: 'less',
+			patterns: [
+				// 这个是加上自己的路径,不能使用(如下:alias)中配置的别名路径
+				path.resolve(__dirname, './src/css/less-variable.less')
+			]
+		}
+	},
+	configureWebpack: config => {
+		config.optimization.minimizer[0].options.terserOptions.compress.drop_console = process.env.NODE_ENV ===
+			'production'
+		config.entry.app = ["babel-polyfill", "./src/main.js"];
+	},
+	configureWebpack: {
+		output: { // 输出重构  打包编译后的 文件名称  【模块名称.版本号.js】
+			filename: `js/[name].${Timestamp}.js`,
+			chunkFilename: `js/[name].${Timestamp}.js`
+			// chunkFilename: `js/[id].vw.js`
+		}
+	}
 }

+ 90 - 5
TEAMModelOS/Controllers/Syllabus/SyllabusController.cs

@@ -14,6 +14,7 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using Microsoft.AspNetCore.Http;
 using TEAMModelOS.SDK.Models.Cosmos.Common;
+using Azure.Cosmos;
 
 namespace TEAMModelOS.Controllers
 {
@@ -33,7 +34,80 @@ namespace TEAMModelOS.Controllers
             _azureCosmos = azureCosmos;
         }
 
-        // TODO 代码优化
+    /*
+    {
+      "id": "hbcn108774752059002880",
+      "code": "Syllabus-hbcn",
+      "trees": [
+          {
+              "id": "111111-111-44E7-8DD9-A6CB6D0D52F2",
+              "pid": "hbcn108515325535981568",
+              "title": "第一章",
+              "order": 0,
+              "type": 1,
+              "children": [
+                  {
+                      "id": "22222-111-447E-8A52-BFCD63E61D33",
+                      "pid": "111111-111-44E7-8DD9-A6CB6D0D52F2",
+                      "title": "第一节333",
+                      "order": 0,
+                      "type": 1,
+                      "children": [
+                          {
+                              "id": "3333-111-447E-8A52-BFCD63E61D33",
+                              "pid": "22222-111-447E-8A52-BFCD63E61D33",
+                              "title": "第一节333",
+                              "order": 0,
+                              "type": 1
+                          },
+                          {
+                              "id": "4444-111-447E-8A52-BFCD63E61D33",
+                              "pid": "22222-111-447E-8A52-BFCD63E61D33",
+                              "title": "第一节333",
+                              "order": 0,
+                              "type": 1
+                          }
+                      ]
+                  }
+              ]
+          },
+          {
+              "id": "111111-222-44E7-8DD9-A6CB6D0D52F2",
+              "pid": "hbcn108515325535981568",
+              "title": "第一章",
+              "order": 0,
+              "type": 1,
+              "children": [
+                  {
+                      "id": "22222-222-447E-8A52-BFCD63E61D33",
+                      "pid": "111111-222-44E7-8DD9-A6CB6D0D52F2",
+                      "title": "第一节333",
+                      "order": 0,
+                      "type": 1,
+                      "children": [
+                          {
+                              "id": "3333-222-447E-8A52-BFCD63E61D33",
+                              "pid": "22222-222-447E-8A52-BFCD63E61D33",
+                              "title": "第一节333",
+                              "order": 0,
+                              "type": 1
+                          }
+                      ]
+                  },
+                  {
+                      "id": "5555-222-447E-8A52-BFCD63E61D33",
+                      "pid": "111111-222-44E7-8DD9-A6CB6D0D52F2",
+                      "title": "第一节333",
+                      "order": 0,
+                      "type": 1,
+                      "children": []
+                  }
+              ]
+          }
+      ]
+    }
+
+          */
 
         /// <summary>
         /// 批量保存或更新课纲
@@ -78,6 +152,20 @@ namespace TEAMModelOS.Controllers
         [HttpPost("delete")]
         public async Task<IActionResult> Delete(JsonElement request)
         {
+            if (!request.TryGetProperty("id", out JsonElement id)) return BadRequest();
+            if (!request.TryGetProperty("code", out JsonElement code)) return BadRequest();
+            if (!request.TryGetProperty("scope", out JsonElement scope)) return BadRequest();
+            var client = _azureCosmos.GetCosmosClient();
+            if (scope.ToString().Equals("school"))
+            {
+                var response = await client.GetContainer("TEAMModelOS", "School").DeleteItemStreamAsync(id.ToString(), new PartitionKey($"Volume-{code}"));
+                return Ok(new { code = response.Status });
+            }
+            else
+            {
+                var response = await client.GetContainer("TEAMModelOS", "Teacher").DeleteItemStreamAsync(id.ToString(), new PartitionKey($"Volume-{code}"));
+                return Ok(new { code = response.Status });
+            }
             return Ok();
             //Dictionary<string, object> dict = new Dictionary<string, object>();
             //var emobj = request.EnumerateObject();
@@ -136,12 +224,10 @@ namespace TEAMModelOS.Controllers
         {
             List<Tnode> nodes = new List<Tnode>();
             Syllabus syllabus = new Syllabus();
-
             TreeToList(request.trees, nodes);
-            // List<SyllabusNode> nods = nodes.ToJson().FromJson<List<SyllabusNode>>() ;
             syllabus.children = nodes;
             syllabus.id = !string.IsNullOrEmpty(request.id) ? request.id : Guid.NewGuid().ToString() ;
-            syllabus.code = request.code;
+            syllabus.code =$"Syllabus-{request.code}" ;
             syllabus.pk = "Syllabus";
             syllabus.ttl = -1;
             await _azureCosmos.GetCosmosClient().GetContainer("TEAMModelOS", "School").UpsertItemAsync<Syllabus>(syllabus,new Azure.Cosmos.PartitionKey(syllabus.code));
@@ -162,7 +248,6 @@ namespace TEAMModelOS.Controllers
             }
             return nodes;
         }
-          
 
         private List<Tnode> TreeToList(List<SyllabusTree> trees, List<Tnode> nodes)
         {