ソースを参照

Merge remote-tracking branch 'origin/master'

lmm 1 週間 前
コミット
2da8ebd3f0
34 ファイル変更2159 行追加429 行削除
  1. 8 0
      package.json
  2. 264 19
      pnpm-lock.yaml
  3. 8 0
      public/templates/templete.docx
  4. 11 0
      src/App.vue
  5. 1 1
      src/api/system/dept/index.ts
  6. 2 1
      src/api/system/studentSelectSupervisorRecord/index.ts
  7. 43 0
      src/api/system/studentSelectSupervisorRecord/selectionBook.ts
  8. 5 0
      src/api/system/studentSelectionProject/index.ts
  9. 4 0
      src/api/system/supervisorSelectionSetting/index.ts
  10. 5 0
      src/api/system/user/index.ts
  11. 4 0
      src/api/system/user/pop.ts
  12. 1 0
      src/api/system/user/profile.ts
  13. 24 1
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  14. 16 0
      src/router/modules/remaining.ts
  15. 2 1
      src/styles/var.css
  16. 50 0
      src/utils/doc.js
  17. 4 0
      src/utils/download.ts
  18. 91 0
      src/utils/htmlToPDF.js
  19. 1 1
      src/views/Login/Login.vue
  20. 2 1
      src/views/Login/components/LoginForm.vue
  21. 52 5
      src/views/system/Home/Index.vue
  22. 133 0
      src/views/system/studentExcused/index.vue
  23. 75 19
      src/views/system/studentSelectSupervisorRecord/index.vue
  24. 402 86
      src/views/system/studentSelectSupervisorRecord/record.vue
  25. 510 83
      src/views/system/studentSelectSupervisorRecord/studentSelectSupervisorRecordForm.vue
  26. 7 8
      src/views/system/studentSelectionProject/index.vue
  27. 1 1
      src/views/system/studentSelf/SForm.vue
  28. 52 88
      src/views/system/studentSelf/index.vue
  29. 13 12
      src/views/system/supervisorSelectionSetting/index.vue
  30. 192 0
      src/views/system/userDetail/student.vue
  31. 163 96
      src/views/system/userDetail/teacher.vue
  32. 1 1
      src/views/system/workroomTeacher/dept/index.vue
  33. 1 1
      src/views/system/workroomTeacher/dept/student.vue
  34. 11 4
      src/views/system/workroomTeacher/user/UserForm.vue

+ 8 - 0
package.json

@@ -44,13 +44,19 @@
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
+    "docxtemplater": "^3.55.8",
     "driver.js": "^1.3.1",
     "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
     "element-plus": "2.8.0",
     "fast-xml-parser": "^4.3.2",
+    "file-saver": "^2.0.5",
     "highlight.js": "^11.9.0",
+    "html2canvas": "^1.4.1",
     "jsencrypt": "^3.3.2",
+    "jspdf": "^2.5.2",
+    "jszip": "^3.10.1",
+    "jszip-utils": "^0.1.0",
     "lodash-es": "^4.17.21",
     "markdown-it": "^14.1.0",
     "markmap-common": "^0.16.0",
@@ -62,6 +68,8 @@
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
+    "pizzip": "^3.1.7",
+    "print-js": "^1.6.0",
     "qrcode": "^1.5.3",
     "qs": "^6.12.0",
     "steady-xml": "^0.1.0",

+ 264 - 19
pnpm-lock.yaml

@@ -65,6 +65,9 @@ importers:
       diagram-js:
         specifier: ^12.8.0
         version: 12.8.1
+      docxtemplater:
+        specifier: ^3.55.8
+        version: 3.55.8
       driver.js:
         specifier: ^1.3.1
         version: 1.3.1
@@ -80,12 +83,27 @@ importers:
       fast-xml-parser:
         specifier: ^4.3.2
         version: 4.5.0
+      file-saver:
+        specifier: ^2.0.5
+        version: 2.0.5
       highlight.js:
         specifier: ^11.9.0
         version: 11.10.0
+      html2canvas:
+        specifier: ^1.4.1
+        version: 1.4.1
       jsencrypt:
         specifier: ^3.3.2
         version: 3.3.2
+      jspdf:
+        specifier: ^2.5.2
+        version: 2.5.2
+      jszip:
+        specifier: ^3.10.1
+        version: 3.10.1
+      jszip-utils:
+        specifier: ^0.1.0
+        version: 0.1.0
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
@@ -119,6 +137,12 @@ importers:
       pinia-plugin-persistedstate:
         specifier: ^3.2.1
         version: 3.2.3(pinia@2.2.6(typescript@5.3.3)(vue@3.4.21(typescript@5.3.3)))
+      pizzip:
+        specifier: ^3.1.7
+        version: 3.1.7
+      print-js:
+        specifier: ^1.6.0
+        version: 1.6.0
       qrcode:
         specifier: ^1.5.3
         version: 1.5.4
@@ -1343,42 +1367,36 @@ packages:
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm-musl@2.5.0':
     resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-linux-arm64-glibc@2.5.0':
     resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm64-musl@2.5.0':
     resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-linux-x64-glibc@2.5.0':
     resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-x64-musl@2.5.0':
     resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-win32-arm64@2.5.0':
     resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@@ -1478,55 +1496,46 @@ packages:
     resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm-musleabihf@4.27.2':
     resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-arm64-gnu@4.27.2':
     resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm64-musl@4.27.2':
     resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-powerpc64le-gnu@4.27.2':
     resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==}
     cpu: [ppc64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-gnu@4.27.2':
     resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==}
     cpu: [riscv64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-s390x-gnu@4.27.2':
     resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==}
     cpu: [s390x]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-gnu@4.27.2':
     resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-musl@4.27.2':
     resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-win32-arm64-msvc@4.27.2':
     resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==}
@@ -1569,28 +1578,24 @@ packages:
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@swc/core-linux-arm64-musl@1.9.2':
     resolution: {integrity: sha512-8xzrOmsyCC1zrx2Wzx/h8dVsdewO1oMCwBTLc1gSJ/YllZYTb04pNm6NsVbzUX2tKddJVRgSJXV10j/NECLwpA==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@swc/core-linux-x64-gnu@1.9.2':
     resolution: {integrity: sha512-kZrNz/PjRQKcchWF6W292jk3K44EoVu1ad5w+zbS4jekIAxsM8WwQ1kd+yjUlN9jFcF8XBat5NKIs9WphJCVXg==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@swc/core-linux-x64-musl@1.9.2':
     resolution: {integrity: sha512-TTIpR4rjMkhX1lnFR+PSXpaL83TrQzp9znRdp2TzYrODlUd/R20zOwSo9vFLCyH6ZoD47bccY7QeGZDYT3nlRg==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@swc/core-win32-arm64-msvc@1.9.2':
     resolution: {integrity: sha512-+Eg2d4icItKC0PMjZxH7cSYFLWk0aIp94LNmOw6tPq0e69ax6oh10upeq0D1fjWsKLmOJAWEvnXlayZcijEXDw==}
@@ -1767,6 +1772,9 @@ packages:
   '@types/qs@6.9.17':
     resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
 
+  '@types/raf@3.4.3':
+    resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
+
   '@types/semver@7.5.8':
     resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
 
@@ -2228,6 +2236,10 @@ packages:
     resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
     engines: {node: '>=10.0.0'}
 
+  '@xmldom/xmldom@0.9.6':
+    resolution: {integrity: sha512-Su4xcxR0CPGwlDHNmVP09fqET9YxbyDXHaSob6JlBH7L6reTYaeim6zbk9o08UarO0L5GTRo3uzl0D+9lSxmvw==}
+    engines: {node: '>=14.6'}
+
   '@zxcvbn-ts/core@3.0.4':
     resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==}
 
@@ -2393,6 +2405,10 @@ packages:
   balanced-match@2.0.0:
     resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
 
+  base64-arraybuffer@1.0.2:
+    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
+    engines: {node: '>= 0.6.0'}
+
   base@0.11.2:
     resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
     engines: {node: '>=0.10.0'}
@@ -2456,6 +2472,11 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
+  btoa@1.2.1:
+    resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
+    engines: {node: '>= 0.4.0'}
+    hasBin: true
+
   buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
 
@@ -2485,6 +2506,10 @@ packages:
   caniuse-lite@1.0.30001680:
     resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==}
 
+  canvg@3.0.10:
+    resolution: {integrity: sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==}
+    engines: {node: '>=10.0.0'}
+
   chalk@1.1.3:
     resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
     engines: {node: '>=0.10.0'}
@@ -2658,6 +2683,9 @@ packages:
   core-js@3.39.0:
     resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
 
+  core-util-is@1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
   cors@2.8.5:
     resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
     engines: {node: '>= 0.10'}
@@ -2696,6 +2724,9 @@ packages:
     resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
     engines: {node: '>=12 || >=16'}
 
+  css-line-break@2.1.0:
+    resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
+
   css-select@4.3.0:
     resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
 
@@ -2989,6 +3020,10 @@ packages:
     resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
 
+  docxtemplater@3.55.8:
+    resolution: {integrity: sha512-sDfZA//rrycVK2nSL763ek5+q9O3yzBhJTB+GWG+NBERL/PbYZGVeFl1+nj+fkFblMWo07NCVqM+HVyuHYbQ9Q==}
+    engines: {node: '>=0.10'}
+
   dom-serializer@0.2.2:
     resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
 
@@ -3024,6 +3059,9 @@ packages:
   domify@1.4.2:
     resolution: {integrity: sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==}
 
+  dompurify@2.5.8:
+    resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==}
+
   dompurify@3.2.0:
     resolution: {integrity: sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==}
 
@@ -3318,6 +3356,9 @@ packages:
   fastq@1.17.1:
     resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
 
+  fflate@0.8.2:
+    resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
   file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
@@ -3326,6 +3367,9 @@ packages:
     resolution: {integrity: sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==}
     engines: {node: '>=18'}
 
+  file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+
   filelist@1.0.4:
     resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
 
@@ -3584,6 +3628,10 @@ packages:
   html-void-elements@2.0.1:
     resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
 
+  html2canvas@1.4.1:
+    resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
+    engines: {node: '>=8.0.0'}
+
   htmlparser2@3.10.1:
     resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
 
@@ -3617,6 +3665,9 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
+  immediate@3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
   immer@9.0.21:
     resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
 
@@ -3918,6 +3969,15 @@ packages:
     resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
     engines: {'0': node >= 0.2.0}
 
+  jspdf@2.5.2:
+    resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==}
+
+  jszip-utils@0.1.0:
+    resolution: {integrity: sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==}
+
+  jszip@3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+
   katex@0.16.11:
     resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
     hasBin: true
@@ -3954,6 +4014,9 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
+  lie@3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
   lilconfig@3.1.2:
     resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
     engines: {node: '>=14'}
@@ -4407,6 +4470,12 @@ packages:
   package-manager-detector@0.2.2:
     resolution: {integrity: sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==}
 
+  pako@1.0.11:
+    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+
+  pako@2.1.0:
+    resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
@@ -4471,6 +4540,9 @@ packages:
   perfect-debounce@1.0.0:
     resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
 
+  performance-now@2.1.0:
+    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -4504,6 +4576,9 @@ packages:
       typescript:
         optional: true
 
+  pizzip@3.1.7:
+    resolution: {integrity: sha512-VemVeAQtdIA74AN1Fsd5OmbMbEeS4YOwwlcudgzvmUrOIOPrk1idYC5Tw5FUFq/I0c26ziNOw9z//iPmGfp1jA==}
+
   pkcs7@1.0.4:
     resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==}
     hasBin: true
@@ -4622,10 +4697,16 @@ packages:
     resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
+  print-js@1.6.0:
+    resolution: {integrity: sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==}
+
   prismjs@1.29.0:
     resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
     engines: {node: '>=6'}
 
+  process-nextick-args@2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
   process@0.11.10:
     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
     engines: {node: '>= 0.6.0'}
@@ -4664,12 +4745,18 @@ packages:
   queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
+  raf@3.4.1:
+    resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
+
   rd@2.0.1:
     resolution: {integrity: sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==}
 
   react-is@18.3.1:
     resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
 
+  readable-stream@2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
   readable-stream@3.6.2:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
@@ -4689,6 +4776,9 @@ packages:
   regenerate@1.4.2:
     resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
 
+  regenerator-runtime@0.13.11:
+    resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+
   regenerator-runtime@0.14.1:
     resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
 
@@ -4775,6 +4865,10 @@ packages:
   rfdc@1.4.1:
     resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
 
+  rgbcolor@1.0.1:
+    resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
+    engines: {node: '>= 0.8.15'}
+
   rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     deprecated: Rimraf versions prior to v4 are no longer supported
@@ -4814,6 +4908,9 @@ packages:
     resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==}
     engines: {node: '>=0.4'}
 
+  safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
   safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
 
@@ -4877,6 +4974,9 @@ packages:
     resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
     engines: {node: '>=0.10.0'}
 
+  setimmediate@1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+
   shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -4981,6 +5081,10 @@ packages:
     resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
     deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
 
+  stackblur-canvas@2.7.0:
+    resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
+    engines: {node: '>=0.1.14'}
+
   static-extend@0.1.2:
     resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
     engines: {node: '>=0.10.0'}
@@ -5020,6 +5124,9 @@ packages:
     resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
     engines: {node: '>= 0.4'}
 
+  string_decoder@1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
   string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
@@ -5101,6 +5208,10 @@ packages:
   svg-baker@1.7.0:
     resolution: {integrity: sha512-nibslMbkXOIkqKVrfcncwha45f97fGuAOn1G99YwnwTj8kF9YiM6XexPcUso97NxOm6GsP0SIvYVIosBis1xLg==}
 
+  svg-pathdata@6.0.3:
+    resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
+    engines: {node: '>=12.0.0'}
+
   svg-tags@1.0.0:
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
 
@@ -5136,6 +5247,9 @@ packages:
     resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
     engines: {node: '>=8'}
 
+  text-segmentation@1.0.3:
+    resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
+
   text-table@0.2.0:
     resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
 
@@ -5356,6 +5470,9 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  utrie@1.0.2:
+    resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
+
   uuid@10.0.0:
     resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
     hasBin: true
@@ -7177,6 +7294,9 @@ snapshots:
 
   '@types/qs@6.9.17': {}
 
+  '@types/raf@3.4.3':
+    optional: true
+
   '@types/semver@7.5.8': {}
 
   '@types/svgo@2.6.4':
@@ -7886,6 +8006,8 @@ snapshots:
 
   '@xmldom/xmldom@0.8.10': {}
 
+  '@xmldom/xmldom@0.9.6': {}
+
   '@zxcvbn-ts/core@3.0.4':
     dependencies:
       fastest-levenshtein: 1.0.16
@@ -8055,6 +8177,8 @@ snapshots:
 
   balanced-match@2.0.0: {}
 
+  base64-arraybuffer@1.0.2: {}
+
   base@0.11.2:
     dependencies:
       cache-base: 1.0.1
@@ -8157,6 +8281,8 @@ snapshots:
       node-releases: 2.0.18
       update-browserslist-db: 1.1.1(browserslist@4.24.2)
 
+  btoa@1.2.1: {}
+
   buffer-from@1.1.2: {}
 
   cac@6.7.14: {}
@@ -8189,6 +8315,18 @@ snapshots:
 
   caniuse-lite@1.0.30001680: {}
 
+  canvg@3.0.10:
+    dependencies:
+      '@babel/runtime': 7.26.0
+      '@types/raf': 3.4.3
+      core-js: 3.39.0
+      raf: 3.4.1
+      regenerator-runtime: 0.13.11
+      rgbcolor: 1.0.1
+      stackblur-canvas: 2.7.0
+      svg-pathdata: 6.0.3
+    optional: true
+
   chalk@1.1.3:
     dependencies:
       ansi-styles: 2.2.1
@@ -8372,6 +8510,8 @@ snapshots:
 
   core-js@3.39.0: {}
 
+  core-util-is@1.0.3: {}
+
   cors@2.8.5:
     dependencies:
       object-assign: 4.1.1
@@ -8411,6 +8551,10 @@ snapshots:
 
   css-functions-list@3.2.3: {}
 
+  css-line-break@2.1.0:
+    dependencies:
+      utrie: 1.0.2
+
   css-select@4.3.0:
     dependencies:
       boolbase: 1.0.0
@@ -8740,6 +8884,10 @@ snapshots:
     dependencies:
       esutils: 2.0.3
 
+  docxtemplater@3.55.8:
+    dependencies:
+      '@xmldom/xmldom': 0.9.6
+
   dom-serializer@0.2.2:
     dependencies:
       domelementtype: 2.3.0
@@ -8781,6 +8929,9 @@ snapshots:
 
   domify@1.4.2: {}
 
+  dompurify@2.5.8:
+    optional: true
+
   dompurify@3.2.0: {}
 
   domutils@1.7.0:
@@ -9224,6 +9375,8 @@ snapshots:
     dependencies:
       reusify: 1.0.4
 
+  fflate@0.8.2: {}
+
   file-entry-cache@6.0.1:
     dependencies:
       flat-cache: 3.2.0
@@ -9232,6 +9385,8 @@ snapshots:
     dependencies:
       flat-cache: 5.0.0
 
+  file-saver@2.0.5: {}
+
   filelist@1.0.4:
     dependencies:
       minimatch: 5.1.6
@@ -9493,6 +9648,11 @@ snapshots:
 
   html-void-elements@2.0.1: {}
 
+  html2canvas@1.4.1:
+    dependencies:
+      css-line-break: 2.1.0
+      text-segmentation: 1.0.3
+
   htmlparser2@3.10.1:
     dependencies:
       domelementtype: 1.3.1
@@ -9527,6 +9687,8 @@ snapshots:
 
   image-size@0.5.5: {}
 
+  immediate@3.0.6: {}
+
   immer@9.0.21: {}
 
   immutable@5.0.2: {}
@@ -9778,6 +9940,27 @@ snapshots:
 
   jsonparse@1.3.1: {}
 
+  jspdf@2.5.2:
+    dependencies:
+      '@babel/runtime': 7.26.0
+      atob: 2.1.2
+      btoa: 1.2.1
+      fflate: 0.8.2
+    optionalDependencies:
+      canvg: 3.0.10
+      core-js: 3.39.0
+      dompurify: 2.5.8
+      html2canvas: 1.4.1
+
+  jszip-utils@0.1.0: {}
+
+  jszip@3.10.1:
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+
   katex@0.16.11:
     dependencies:
       commander: 8.3.0
@@ -9809,6 +9992,10 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
+  lie@3.3.0:
+    dependencies:
+      immediate: 3.0.6
+
   lilconfig@3.1.2: {}
 
   lines-and-columns@1.2.4: {}
@@ -10294,6 +10481,10 @@ snapshots:
 
   package-manager-detector@0.2.2: {}
 
+  pako@1.0.11: {}
+
+  pako@2.1.0: {}
+
   parent-module@1.0.1:
     dependencies:
       callsites: 3.1.0
@@ -10345,6 +10536,9 @@ snapshots:
 
   perfect-debounce@1.0.0: {}
 
+  performance-now@2.1.0:
+    optional: true
+
   picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}
@@ -10365,6 +10559,10 @@ snapshots:
     optionalDependencies:
       typescript: 5.3.3
 
+  pizzip@3.1.7:
+    dependencies:
+      pako: 2.1.0
+
   pkcs7@1.0.4:
     dependencies:
       '@babel/runtime': 7.26.0
@@ -10486,8 +10684,12 @@ snapshots:
       ansi-styles: 5.2.0
       react-is: 18.3.1
 
+  print-js@1.6.0: {}
+
   prismjs@1.29.0: {}
 
+  process-nextick-args@2.0.1: {}
+
   process@0.11.10: {}
 
   progress@2.0.3: {}
@@ -10517,12 +10719,27 @@ snapshots:
 
   queue-microtask@1.2.3: {}
 
+  raf@3.4.1:
+    dependencies:
+      performance-now: 2.1.0
+    optional: true
+
   rd@2.0.1:
     dependencies:
       '@types/node': 10.17.60
 
   react-is@18.3.1: {}
 
+  readable-stream@2.3.8:
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+
   readable-stream@3.6.2:
     dependencies:
       inherits: 2.0.4
@@ -10541,6 +10758,9 @@ snapshots:
 
   regenerate@1.4.2: {}
 
+  regenerator-runtime@0.13.11:
+    optional: true
+
   regenerator-runtime@0.14.1: {}
 
   regenerator-transform@0.15.2:
@@ -10616,6 +10836,9 @@ snapshots:
 
   rfdc@1.4.1: {}
 
+  rgbcolor@1.0.1:
+    optional: true
+
   rimraf@3.0.2:
     dependencies:
       glob: 7.2.3
@@ -10679,6 +10902,8 @@ snapshots:
       has-symbols: 1.0.3
       isarray: 2.0.5
 
+  safe-buffer@5.1.2: {}
+
   safe-buffer@5.2.1: {}
 
   safe-json-parse@4.0.0:
@@ -10752,6 +10977,8 @@ snapshots:
       is-plain-object: 2.0.4
       split-string: 3.1.0
 
+  setimmediate@1.0.5: {}
+
   shebang-command@2.0.0:
     dependencies:
       shebang-regex: 3.0.0
@@ -10862,6 +11089,9 @@ snapshots:
 
   stable@0.1.8: {}
 
+  stackblur-canvas@2.7.0:
+    optional: true
+
   static-extend@0.1.2:
     dependencies:
       define-property: 0.2.5
@@ -10910,6 +11140,10 @@ snapshots:
       define-properties: 1.2.1
       es-object-atoms: 1.0.0
 
+  string_decoder@1.1.1:
+    dependencies:
+      safe-buffer: 5.1.2
+
   string_decoder@1.3.0:
     dependencies:
       safe-buffer: 5.2.1
@@ -11035,6 +11269,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  svg-pathdata@6.0.3:
+    optional: true
+
   svg-tags@1.0.0: {}
 
   svg.js@2.7.1: {}
@@ -11078,6 +11315,10 @@ snapshots:
 
   text-extensions@2.4.0: {}
 
+  text-segmentation@1.0.3:
+    dependencies:
+      utrie: 1.0.2
+
   text-table@0.2.0: {}
 
   through@2.3.8: {}
@@ -11345,6 +11586,10 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  utrie@1.0.2:
+    dependencies:
+      base64-arraybuffer: 1.0.2
+
   uuid@10.0.0: {}
 
   vary@1.1.2: {}

+ 8 - 0
public/templates/templete.docx

@@ -0,0 +1,8 @@
+名字: {name}
+条件:
+{#conditionCons}
+  条件组: {groupName}
+  内容: {cont}
+{/conditionCons}
+
+ 

+ 11 - 0
src/App.vue

@@ -4,6 +4,7 @@ import { useAppStore } from '@/store/modules/app'
 import { useDesign } from '@/hooks/web/useDesign'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import routerSearch from '@/components/RouterSearch/index.vue'
+import { ref, computed } from 'vue'
 
 defineOptions({ name: 'APP' })
 
@@ -14,6 +15,11 @@ const currentSize = computed(() => appStore.getCurrentSize)
 const greyMode = computed(() => appStore.getGreyMode)
 const { wsCache } = useCache()
 
+// 设备判断
+const isMobile = computed(() => {
+  return window.innerWidth >= 768 // 假设768px以下为移动端
+})
+
 // 根据浏览器当前主题设置系统主题色
 const setDefaultTheme = () => {
   let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
@@ -26,6 +32,11 @@ setDefaultTheme()
 </script>
 <template>
   <ConfigGlobal :size="currentSize">
+    <div v-if="!isMobile">
+      <Menu /> <!-- 菜单组件 -->
+      <Breadcrumb /> <!-- 面包屑组件 -->
+      <Navbar /> <!-- 导航栏组件 -->
+    </div>
     <RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
     <routerSearch />
   </ConfigGlobal>

+ 1 - 1
src/api/system/dept/index.ts

@@ -68,4 +68,4 @@ export const deleteDept = async (id: number) => {
 // 获取毕业达成率
 export const getGraduationSource = async () => {
   return await request.get({ url: '/system/dept/getGraduationSource' })
-}
+}

+ 2 - 1
src/api/system/studentSelectSupervisorRecord/index.ts

@@ -12,7 +12,8 @@ export interface studentSelectSupervisorRecordVO {
   supervisorSignature: string // 导师电子签名
   externalSupervisorId: number // 校外导师id
   masterType:number//硕士类型
-  agreeDate:Date//同意日期
+  studentSignDate:Date//学生签字日期
+  supervisorSignDate:Date//导师签字日期
   supervisorType:string//导师类型
 }
 

+ 43 - 0
src/api/system/studentSelectSupervisorRecord/selectionBook.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// 师生互选记录 VO
+export interface selectionBookVO {
+  studentName: string; // 学生姓名
+  studentNumber: string; // 学生学号
+  major: string; // 学生专业
+  supervisorMajor: string; // 导师研究方向
+  studentMobile: string; // 学生联系电话
+  supervisor: string; // 导师名称
+  studentAchievementRequirement: string; // 学生成果要求
+  introduction: string; // 简介
+  academicSlots: number; // 学硕名额
+  professionalSlots: number; // 专硕名额
+  supervisorSignature:string;//导师签名
+  studentSignature:string;//学生签名
+  studentSignDate:Date;//学生签名日期
+  supervisorSignDate:Date;//学生签名日期
+}
+
+export interface printBookVO {
+  id:string;
+  studentName:string;
+  studentNumber:string;
+  major:string;
+  studentMobile :string;
+  supervisor:string;
+  supervisorType:string;
+  studentAchievementRequirement:string;
+  studentSignDate:Date
+  supervisorSignDate:Date
+}
+
+export const selectionBookApi = {
+
+  //更新互选信息表消息
+  updateSelectionBook: async (data: selectionBookVO) => {
+    return await request.put({ url: `/system/student-select-supervisor-record/updateSelectionBook`, data })
+  },
+  getSelectionBook: async (id: number) => {
+    return await request.get({ url: `/system/student-select-supervisor-record/getSelectionBook?id=`+ id })
+  },
+}

+ 5 - 0
src/api/system/studentSelectionProject/index.ts

@@ -42,4 +42,9 @@ export const studentSelectionProjectApi = {
   exportStudentSelectionProject: async (params) => {
     return await request.download({ url: `/system/student-selection-project/export-excel`, params })
   },
+
+  // 获取所有项目
+  getAllProject: async () => {
+    return await request.get({ url: `/system/student-selection-project/getAllProject`})
+  },
 }

+ 4 - 0
src/api/system/supervisorSelectionSetting/index.ts

@@ -44,4 +44,8 @@ export const supervisorSelectionSettingApi = {
   exportSupervisorSelectionSetting: async (params) => {
     return await request.download({ url: `/system/supervisor-selection-setting/export-excel`, params })
   },
+
+  getSupervisorInfo: async (supervisorId: number, projectId: number) => {
+    return await request.get({url: `/system/supervisor-selection-setting/getSupervisorInfo?supervisorId=${supervisorId}&projectId=${projectId}`});
+  }
 }

+ 5 - 0
src/api/system/user/index.ts

@@ -21,6 +21,8 @@ export interface UserVO {
   photoUrl: string// 学生照片
   userNumber: string// 学号
   photoExist: number// 照片是否存在
+  title:string//职称
+  studentAchievementRequirement:string//学生成果要求
 }
 
 // 查询用户管理列表
@@ -35,13 +37,16 @@ export const getTeacherPage = (params: PageParam) => {
 export const getTeacherPageForTeacher = (params: PageParam) => {
   return request.get({ url: '/system/user/getTeacherPageForTeacher', params })
 }
+
 // 查询学生管理列表
 export const getStudentPage = (params: PageParam) => {
   return request.get({ url: '/system/user/page1', params })
 }
+
 export const getUserPageForTeacher = (params: PageParam) => {
   return request.get({ url: '/system/user/pageForTeacher', params })
 }
+
 // 查询毕业生列表
 export const getGraduateStudentPage = (params: PageParam) => {
   return request.get({ url: '/system/user/graduateStudentPage', params })

+ 4 - 0
src/api/system/user/pop.ts

@@ -15,3 +15,7 @@ export const getPopData = () => {
 export const updateIsPop = (data: PopDo) => {
   return request.put({ url: '/system/user/updateIsPop', data })
 }
+
+export const getIsPop = () => {
+  return request.get({ url: '/system/user/getIsPop' })
+}

+ 1 - 0
src/api/system/user/profile.ts

@@ -36,6 +36,7 @@ export interface ProfileVO {
   masterType: string
   userType: string
   photoUrl: string
+  isSelectionInformation:string
 }
 
 export interface UserProfileUpdateReqVO {

+ 24 - 1
src/components/ConfigGlobal/src/ConfigGlobal.vue

@@ -8,7 +8,7 @@ import { useAppStore } from '@/store/modules/app'
 import { setCssVar } from '@/utils'
 import { useDesign } from '@/hooks/web/useDesign'
 
-const { variables } = useDesign()
+const { getPrefixCls,variables } = useDesign()
 
 const appStore = useAppStore()
 
@@ -33,6 +33,29 @@ watch(
       !appStore.getMobile ? appStore.setMobile(true) : undefined
       setCssVar('--left-menu-min-width', '0')
       appStore.setCollapse(true)
+      const prefixCls = getPrefixCls('tool-header')
+      // await document.getElementsByClassName(prefixCls)[0].style.display = 'none'
+      // document.getElementById(prefixCls)?.style.display = 'none'
+      const callback = (mutationsList, observer) => {
+        for (let mutation of mutationsList) {
+          if (mutation.type === 'childList' && mutation.addedNodes.length) {
+            const elements = document.getElementsByClassName(prefixCls);
+            const tagsView = document.getElementById("v-tags-view");
+            
+            if (elements.length > 0 && tagsView != null) {
+              // console.log(prefixCls, elements, elements[0]);
+              elements[0].style.display = 'none';
+              tagsView.style.display = 'none';
+              // 执行你需要的操作
+              observer.disconnect(); // 停止观察
+              return;
+            }
+          }
+        }
+      };
+
+      const observer = new MutationObserver(callback);
+      observer.observe(document, { childList: true, subtree: true });
       appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
     } else {
       appStore.getMobile ? appStore.setMobile(false) : undefined

+ 16 - 0
src/router/modules/remaining.ts

@@ -5,6 +5,7 @@ import deptInfo from '@/views/system/workroomCollege/deptInfo/index.vue'
 import * as LoginApi from '@/api/login'
 import supervisorSelectionSetting from "@/views/system/supervisorSelectionSetting/index.vue";
 import studentSelectSupervisorRecord from "@/views/system/studentSelectSupervisorRecord/index.vue";
+import studentForm from "@/views/system/userDetail/student.vue";
 
 const { t } = useI18n()
 /**
@@ -59,6 +60,21 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
 
+      {
+        path: 'userDetail/student',
+        component: studentForm,
+        name: 'studentForm',
+        meta: {
+          title: '互选信息填写',
+          icon: 'ep:user-filled',
+          noCache: false,
+          affix: false,
+          breadcrumb: false,
+          noTagsView: false,
+          hidden: true
+        }
+      },
+
       {
         path: 'studentSelectSupervisorRecord/index',
         component: studentSelectSupervisorRecord,

+ 2 - 1
src/styles/var.css

@@ -53,6 +53,7 @@
   --app-footer-height: 50px;
 
   --transition-time-02: 0.2s;
+
 }
 
 .dark {
@@ -71,4 +72,4 @@ body {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
-}
+}

+ 50 - 0
src/utils/doc.js

@@ -0,0 +1,50 @@
+import Docxtemplater from 'docxtemplater'
+import PizZip from 'pizzip'
+import JSZipUtils from 'jszip-utils'
+import { saveAs } from 'file-saver'
+ 
+/**
+ 4. 导出docx
+ 5. @param { String } tempDocxPath 模板文件路径
+ 6. @param { Object } data 文件中传入的数据
+ 7. @param { String } fileName 导出文件名称
+*/
+export const exportDocx = (tempDocxPath, data, fileName) => {
+  // 读取并获得模板文件的二进制内容
+  JSZipUtils.getBinaryContent(tempDocxPath, (error, content) => {
+    if (error) {
+        console.error("获取文件时出现错误:", error);
+        throw error;
+    }
+    console.log("Content type:", typeof content);
+    console.log("获取到的内容:", content); 
+    console.log("获取到的内容长度:", content.byteLength);
+    const uint8Array = new Uint8Array(content);
+    console.log("文件内容字节:", uint8Array);
+    const zip = new PizZip(content)
+    const doc = new Docxtemplater().loadZip(zip)
+    console.log(doc)
+    doc.setData(data)
+    try {
+      // render the document (replace all occurences of {first_name} by John, {last_name} by Doe, ...)
+      doc.render()
+    } catch (error) {
+      const e = {
+        message: error.message,
+        name: error.name,
+        stack: error.stack,
+        properties: error.properties
+      }
+      console.log({
+        error: e
+      })
+      // The error thrown here contains additional information when logged with JSON.stringify (it contains a property object).
+      throw error
+    }
+    const out = doc.getZip().generate({
+      type: 'blob',
+      mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+    }) // Output the document using Data-URI
+    saveAs(out, fileName)
+  })
+}

+ 4 - 0
src/utils/download.ts

@@ -33,6 +33,10 @@ const download = {
   markdown: (data: Blob, fileName: string) => {
     download0(data, fileName, 'text/markdown')
   },
+  //新加的
+  pdf: (data: Blob, fileName: string) => {
+    download0(data, fileName, 'application/pdf')
+  },
   // 下载图片(允许跨域)
   image: ({
     url,

+ 91 - 0
src/utils/htmlToPDF.js

@@ -0,0 +1,91 @@
+// 页面导出为pdf格式 //title表示为下载的标题,html表示document.querySelector('#myPrintHtml')
+import html2Canvas from 'html2canvas'
+import JsPDF from 'jspdf'
+var noTableHeight = 0 //table外的元素高度
+
+export const htmlPdf = (title, html, fileList, type) => {// type传有效值pdf则为横版
+  if (fileList) {
+    const pageHeight = Math.floor(277 * html.scrollWidth / 190) + 20 //计算pdf高度
+    for (let i = 0; i < fileList.length; i++) { //循环获取的元素
+      const multiple = Math.ceil((fileList[i].offsetTop + fileList[i].offsetHeight) / pageHeight) //元素的高度
+      if (isSplit(fileList, i, multiple * pageHeight)) { //计算是否超出一页
+        var _H = '' //向pdf插入空白块的内容高度
+        if (fileList[i].localName !== 'tr') { //判断是不是表格里的内容
+          _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight)
+        } else {
+          _H = multiple * pageHeight - (fileList[i].offsetTop + fileList[i].offsetHeight + noTableHeight) + 20
+        }
+        var newNode = getFooterElement(_H)  //向pdf插入空白块的内容
+        const divParent = fileList[i].parentNode // 获取该div的父节点
+        const next = fileList[i].nextSibling // 获取div的下一个兄弟节点
+        // 判断兄弟节点是否存在
+        if (next) {
+          // 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
+          divParent.insertBefore(newNode, next)
+        } else {
+          // 否则向节点添加最后一个子节点
+          divParent.appendChild(newNode)
+        }
+      }
+    }
+  }
+  html2Canvas(html, {
+    allowTaint: false,
+    taintTest: false,
+    logging: false,
+    useCORS: true,
+    dpi: window.devicePixelRatio * 2, // 增加DPI以提高图像清晰度
+    scale: 2 // 增加比例以提高图像清晰度
+  }).then(canvas => {
+    var pdf = new JsPDF('p', 'mm', 'a4'); // A4纸,纵向
+    var ctx = canvas.getContext('2d');
+    var a4w = type ? 297 : 210; // A4纸的宽度(mm)
+    var a4h = type ? 210 : 297; // A4纸的高度(mm)
+    var imgHeight = Math.floor((a4h / 297) * canvas.height); // 按A4比例计算图像高度
+    var renderedHeight = 0;
+    while (renderedHeight < canvas.height) {
+      var page = document.createElement('canvas');
+      page.width = canvas.width;
+      page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页
+      // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
+      page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0);
+      // 添加图像到页面,保留10mm边距
+      pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w - 20, Math.min(a4h - 20, (a4w - 20) * page.height / page.width));
+      renderedHeight += imgHeight;
+      if (renderedHeight < canvas.height) {
+        pdf.addPage(); // 如果后面还有内容,添加一个空页
+      }
+    }
+    // 保存文件
+    pdf.save(title + '.pdf');
+    // const pdfBlob = pdf.output('blob'); // 生成 Blob
+    // resolve(pdfBlob); // 返回 Blob
+  });
+}
+// pdf截断需要一个空白位置来补充
+const getFooterElement = (remainingHeight, fillingHeight = 0) => {
+  const newNode = document.createElement('div')
+  newNode.style.background = '#ffffff'
+  newNode.style.width = 'calc(100% + 8px)'
+  newNode.style.marginLeft = '-4px'
+  newNode.style.marginBottom = '0px'
+  newNode.classList.add('divRemove')
+  newNode.style.height = (remainingHeight + fillingHeight) + 'px'
+  return newNode
+}
+const isSplit = (nodes, index, pageHeight) => {
+  // 判断是不是tr 如果不是高度存起来
+  // 表格里的内容要特殊处理
+  // tr.offsetTop 是tr到table表格的高度
+  // 所以计算高速时候要把表格外的高度加起来
+  // 生成的pdf没有表格了这里可以不做处理 直接计算就行
+  if (nodes[index].localName !== 'tr') {  //判断元素是不是tr
+    noTableHeight += nodes[index].clientHeight
+  }
+
+  if (nodes[index].localName !== 'tr') {
+    return nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight
+  } else {
+    return nodes[index].offsetTop + nodes[index].offsetHeight + noTableHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight + noTableHeight > pageHeight
+  }
+}

+ 1 - 1
src/views/Login/Login.vue

@@ -190,7 +190,7 @@ $prefix-cls: #{$namespace}-login;
   }
 }
 .background-image {
-  background-image: url('D:/coding/graduate-ui/src/assets/imgs/GLUT.png');
+  background-image: url('../../assets/imgs/GLUT.png');
   background-size: cover;
   background-position: center;
 }

+ 2 - 1
src/views/Login/components/LoginForm.vue

@@ -152,6 +152,7 @@ import * as authUtil from '@/utils/auth'
 import { usePermissionStore } from '@/store/modules/permission'
 import * as LoginApi from '@/api/login'
 import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
 
 defineOptions({ name: 'LoginForm' })
 
@@ -268,7 +269,7 @@ const handleLogin = async (params) => {
     console.log(userType,"我在这里")
     console.log(userType);  // 查看 userType 的结构
     if (userType && Array.isArray(userType.roles)) {
-        if (userType.roles.indexOf("student") !== -1) {
+        if (userType.roles.indexOf("student") !== -1 ) {
             console.log("我是学生");
             window.location.href = '/system/studentSelf/index';
         } else {

+ 52 - 5
src/views/system/Home/Index.vue

@@ -279,24 +279,28 @@
       <!--        </el-row>-->
 
       <!-- 第二个el-row -->
-
     </el-row>
   </el-skeleton>
+  <teacher ref="teacherFormRef"/>
 </template>
 
 <script lang="ts" setup>
-import {EChartsOption, List} from 'echarts'
+// import {EChartsOption, List} from 'echarts'
 import {formatTime} from '@/utils'
 import {formatDate} from "@/utils/formatTime";
 import {useUserStore} from '@/store/modules/user'
 import {useWatermark} from '@/hooks/web/useWatermark'
 import type {WorkplaceTotal, Project, Notice, Shortcut} from './types'
 import {pieOptions, barOptions} from './echarts-data'
-import {reactive, onMounted, watchEffect} from "vue";
-import {vue3ScrollSeamless} from "vue3-scroll-seamless";
+import {reactive, onMounted, watchEffect, ref, nextTick} from "vue";
+// import {vue3ScrollSeamless} from "vue3-scroll-seamless";
 import * as UserApi from '@/api/system/user'
 import * as  DeptApi from '@/api/system/dept'
 import {StudentAttendanceApi} from '@/api/system/studentAttendance'
+import {getIsPop} from "@/api/system/user/pop";
+import {getUserProfile, ProfileVO} from "@/api/system/user/profile";
+
+import teacher from '../userDetail/teacher.vue';
 
 defineOptions({name: 'SystemHome'})
 
@@ -304,6 +308,9 @@ const {t} = useI18n()
 const userStore = useUserStore()
 const {setWatermark} = useWatermark()
 const loading = ref(true)
+
+const teacherFormRef = ref();
+
 const avatar = userStore.getUser.avatar
 const username = userStore.getUser.nickname
 
@@ -542,7 +549,40 @@ const list1Options = reactive({
   direction: 1,//方向: 0 往下 1 往上 2 向左 3 向右。
 });
 
-onMounted(() => {
+
+// LSQ
+const isPop  = ref();
+const userInfo = ref({} as ProfileVO);
+
+const getUserInfo = async () => {
+  try {
+    loading.value = true;
+    const users = await getUserProfile();
+    userInfo.value = users;
+  } catch (error) {
+    console.error('获取用户信息失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const getIsPopFunction =(async ()=>{
+  isPop.value = await  getIsPop();
+  console.log(isPop.value)
+})
+
+const teacherForm = () => {
+  console.log(teacherFormRef.value)
+  if (teacherFormRef.value) {
+    teacherFormRef.value.open("update",null);
+  } else {
+    console.error('teacher form component is not mounted yet!');
+  }
+};
+
+
+onMounted(async () => {
+  await getUserInfo()
   getDetail()
   getWeekend()
   getProject()
@@ -551,6 +591,13 @@ onMounted(() => {
   getStudentAttendance()
   getGraduationSource()
   getGraduateCount()
+  if(userInfo.value.userType ==='3') {
+    await getIsPopFunction();
+    if (isPop.value ) {
+      await nextTick();
+      teacherForm();
+    }
+  }
 })
 
 const getAllApi = async () => {

+ 133 - 0
src/views/system/studentExcused/index.vue

@@ -0,0 +1,133 @@
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<template class="visible-xs">
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    :id="userId"
+  >
+    <el-row style="margin-top: 5vw;">
+      <el-col :span="24">
+        <el-form-item label="日期" prop="date">
+          <el-date-picker
+            class="!w-full"
+            v-model="formData.date"
+            value-format="YYYY-MM-DD"
+            type="dates"
+            placeholder="选择日期"
+            :disabled-date="disabledDate"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row>
+      <el-col :span="24">
+        <el-form-item label="备注原因" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注原因" />
+      </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row type="flex" justify="center" align="middle" style="margin-top: 5vw;">
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button style="margin-left: 10px;" @click="cancelForm">取 消</el-button>
+    </el-row>
+  </el-form>   
+</template>
+<script setup lang="ts">
+import { FormRules } from 'element-plus'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import { StudentAttendanceApi, StudentAttendanceVO } from '@/api/system/studentAttendance'
+
+/** 学生考勤记录 表单 */
+defineOptions({ name: 'StudentExcused' })
+
+//获取登录人员信息
+const userInfo = ref({} as ProfileVO)
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users;
+  userId.value = users.id;
+}
+const userId = ref('')
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formRef = ref() // 表单的 ref
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  studentName: undefined,
+  userNumber: undefined,
+  supervisor: undefined,
+  deptId: undefined,
+  deptName: undefined,
+  date: undefined,
+  clockInTime: undefined,
+  clockInStatus: undefined,
+  remark: undefined,
+})
+const formRules = reactive<FormRules>({
+  date: [{ required: true, message: '日期不能为空', trigger: 'blur' }],
+  remark:[{ required: true, message: '备注原因不能为空', trigger: 'blur' }],
+})
+
+/** 日期选择器的禁用日期 */
+const disabledDate = (date) => {
+  return date.getTime() < new Date().setHours(0, 0, 0, 0); // 禁用今天之前的日期
+  // return date.getTime() <= new Date().setHours(0, 0, 0, 0); // 禁用今天之前的日期,包括今天
+};
+
+const clockInStatuE = ref('2') // 打卡状态
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  formData.value.clockInStatus = clockInStatuE.value
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as StudentAttendanceVO;
+    const dates = Array.isArray(formData.value.date) ? formData.value.date : [];
+    for (const date of dates) {
+      const submissionData = { ...data, date }; // 为每个日期创建新的提交数据
+      await StudentAttendanceApi.createStudentAttendance(submissionData);
+    }
+    message.success(t('提交成功'));
+    // 发送操作成功的事件
+    emit('success');
+  } finally {
+    formLoading.value = false;
+  }
+}
+
+/** 取消表单 */
+const cancelForm = () => {
+  formRef.value.resetFields()
+}
+
+onMounted(async () => {
+  await getUserInfo()
+  // await getStudentAttendance();
+})
+</script>
+
+<style lang="scss" scoped>
+.visible-xs{        //电脑端css
+
+display: none;
+
+}
+
+@media (max-width: 767px) {        //移动端css
+
+.visible-xs{
+
+display: block !important;
+
+}
+
+}
+</style>

+ 75 - 19
src/views/system/studentSelectSupervisorRecord/index.vue

@@ -123,8 +123,7 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column type="index" width="50" />
-<!--      <el-table-column label="Id" align="center" prop="id" />-->
-<!--      <el-table-column label="项目id" align="center" prop="projectId" />-->
+
       <el-table-column label="学生姓名" align="center" prop="studentName" />
 
       <el-table-column label="硕士类型" align="center" prop="masterType" >
@@ -135,15 +134,25 @@
 
       <el-table-column label="专业" align="center" prop="major" />
 
-      <el-table-column label="简历" align="center" prop="introduction" />
+      <el-table-column label="学生简历" align="center" prop="introduction">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.introduction"
+            type="text"
+            @click="handleDownload(scope.row)"
+          >
+            下载简历
+          </el-button>
+        </template>
+      </el-table-column>
 
       <el-table-column label="申请状态" align="center" prop="selectType" >
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_STUDENT_SELECT_RECORD_SELECT_TYPE" :value="scope.row.selectType" />
         </template>
       </el-table-column>
-      <el-table-column label="学生电子签名" align="center" prop="studentSignature" />
-      <el-table-column label="导师电子签名" align="center" prop="supervisorSignature" />
+      <el-table-column label="学生电子签名" align="center" prop="studentSignature" v-if ="userInfo.userType==='4'" />
+      <el-table-column label="导师电子签名" align="center" prop="supervisorSignature" v-if ="userInfo.userType==='4'" />
       <el-table-column
         label="提交时间"
         align="center"
@@ -164,7 +173,7 @@
           <el-button
             link
             type="primary"
-            @click="handelAgree(scope.row.id)"
+            @click="agreeOpenForm('agree', scope.row.projectId,scope.row.supervisorId,scope.row.id,scope.row.studentId)"
             v-hasPermi="['system:student-select-supervisor-record:agree']"
             v-if="userInfo?.userType === '3' &&scope.row.selectType ==1"
           >
@@ -181,6 +190,16 @@
             退回
           </el-button>
 
+          <el-button
+            link
+            type="primary"
+            @click="handelPrint(scope.row.id)"
+            v-hasPermi="['system:student-select-supervisor-record:getSelection']"
+            v-if="userInfo?.userType === '4' &&scope.row.selectType ==2"
+          >
+            信息
+          </el-button>
+
 <!--          <el-button-->
 <!--            link-->
 <!--            type="primary"-->
@@ -213,6 +232,7 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <studentSelectSupervisorRecordForm ref="formRef" @success="getList" />
+  <studentSelectSupervisorRecordForm ref="agreeRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
@@ -224,6 +244,7 @@ import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
 import {useRoute} from "vue-router";
 import {ref} from "vue";
 import { DICT_TYPE } from '@/utils/dict'
+import {selectionBookVO,printBookVO,selectionBookApi} from '@/api/system/studentSelectSupervisorRecord/selectionBook'
 
 /** 师生互选记录 列表 */
 defineOptions({ name: 'studentSelectSupervisorRecord' })
@@ -266,7 +287,8 @@ const formData = ref({
   supervisorSignature: "",
   externalSupervisorId: undefined,
   masterType:undefined,
-  agreeDate:[]//同意日期
+  studentSignDate:[],
+  supervisorSignDate:[],
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -283,6 +305,7 @@ const getList = async () => {
   loading.value = true
   try {
     const data = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecordPage(queryParams)
+    console.log('data',data); 
     list.value = data.list
     total.value = data.total
   } finally {
@@ -304,8 +327,8 @@ const resetQuery = () => {
 
 /** 添加/修改操作 */
 const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+const openForm = (type: string, projectId?: number,supervisorId? :number,id?:number) => {
+  formRef.value.open(type, projectId,supervisorId,id)
 }
 
 /** 删除按钮操作 */
@@ -336,18 +359,24 @@ const handleExport = async () => {
   }
 }
 
-const handelAgree = async (id:number) => {
-  console.log(id)
-  try {
-    await message.confirm()
-    formData.value.id=id
-    const data =formData.value as unknown as studentSelectSupervisorRecordVO
-    await studentSelectSupervisorRecordApi.agreeStudentSelectSupervisorRecord(data)
-    message.success("通过了申请")
-    await getList()
-  } catch {}
+
+const agreeRef = ref()
+const agreeOpenForm = (type: string, projectId?: number,supervisorId? :number,id?:number,studentId?:number) => {
+  agreeRef.value.open(type, projectId,supervisorId,id,studentId)
 }
 
+// const handelAgree = async (id:number) => {
+//   console.log(id)
+//   try {
+//     await message.confirm()
+//     formData.value.id=id
+//     const data =formData.value as unknown as studentSelectSupervisorRecordVO
+//     await studentSelectSupervisorRecordApi.agreeStudentSelectSupervisorRecord(data)
+//     message.success("通过了申请")
+//     await getList()
+//   } catch {}
+// }
+
 const handelRefuse = async (id:number) => {
   try {
     await message.confirm()
@@ -370,6 +399,33 @@ const getRecordList = async () => {
   }
 }
 
+const handleDownload = (row) => {
+  // window.open(url, '_blank');
+  const url = row.introduction;
+    if (url) {
+      // 创建一个 a 标签并模拟点击,实现下载
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = '学生简历'; // 提示浏览器下载文件
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+    } else {
+      console.error('下载地址无效');
+    }
+};
+
+const handelPrint = async (id:number) => {
+  try {
+    await message.confirm()
+    const data =await selectionBookApi.getSelectionBook(id)as unknown as printBookVO
+    console.log(data)
+    message.success("获取打印信息成功")
+    await getList()
+  } catch {}
+}
+
+
 /** 初始化 **/
 onMounted(() => {
   getUserInfo()

+ 402 - 86
src/views/system/studentSelectSupervisorRecord/record.vue

@@ -1,97 +1,246 @@
 <template>
-  <!--  是学生-->
-  <ContentWrap v-if="userInfo.userType==='1' ||userInfo.userType==='4'" >
-      <el-table
-        v-loading="loading"
-        :data="selectionList"
-        row-key="id"
-      >
-      <el-table-column type="index" width="50" />
-      <el-table-column label="学生姓名" align="center" prop="studentName" />
-      <el-table-column label="硕士类型" align="center" prop="masterType" >
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_STUDENT_MASTER_TYPE" :value="scope.row.masterType" />
-        </template>
-      </el-table-column>
-        <el-table-column label="导师姓名" align="center" prop="supervisor" />
-
-        <el-table-column
-          label="导师类型"
-          align="center"
-          prop="supervisorType"
-          :formatter="userTypeFormatter"
-        />
-
-<!--      <el-table-column label="简历" align="center" prop="introduction" />-->
-
-      <el-table-column label="申请状态" align="center" prop="selectType" >
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_STUDENT_SELECT_RECORD_SELECT_TYPE" :value="scope.row.selectType" />
-        </template>
-      </el-table-column>
-<!--      <el-table-column label="学生电子签名" align="center" prop="studentSignature" />-->
-<!--      <el-table-column label="导师电子签名" align="center" prop="supervisorSignature" />-->
-      <el-table-column
-        label="提交时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column
-        label="导师审批时间"
-        align="center"
-        prop="supervisorApproveTime"
-        :formatter="dateFormatter"
-        width="180px"
+   <!-- <el-loading :loading="isLoading" text="加载中..."> -->
+      <!-- 搜索工作栏 -->
+      <ContentWrap v-if="userInfo.userType==='4'">
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="100px"
+        >
+          <el-form-item label="参与导师" prop="supervisorIds">
+            <el-select
+              v-model="queryParams.supervisorId"
+              @change="getRecordList"
+              placeholder="请选择参与导师"
+              clearable
+              filterable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="supervisor in supervisors"
+                :key="supervisor.id"
+                :label="supervisor.nickname"
+                :value="supervisor.id"
+              />
+            </el-select>
+          </el-form-item>
+        
+          <el-form-item label="请选择项目" prop="projectId">
+            <el-select
+              v-model="queryParams.projectId"
+              @change="getRecordList"
+              placeholder="请选择项目"
+              clearable
+              filterable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="project in projects"
+                :key="project.id"
+                :label="project.projectName"
+                :value="project.id"
+              />
+            </el-select>
+          </el-form-item>
+        
+          <el-form-item label="参与学生年级" prop="studentGrade">
+            <el-select
+              v-model="queryParams.studentGrade"
+              placeholder="请选择参与学生年级"
+              clearable
+              @change="handleQuery"
+              class="!w-240px"
+            >
+              <el-option
+                v-for="year in gradeOptions"
+                :key="year"
+                :label="year"
+                :value="year"
+              />
+            </el-select>
+          </el-form-item>
+        
+          <el-form-item label="学生姓名" v-if="userInfo.userType==='4'">
+            <el-input
+              v-model="queryParams.studentName"
+              placeholder="请输入学生名称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+
+          <el-form-item label="选择状态" prop="selectType">
+            <el-select
+              v-model="queryParams.selectType"
+              @change="getRecordList"
+              placeholder="请选择选择状态"
+              clearable
+              class="!w-240px"
+            >
+              <el-option label="撤回" value="0" />
+              <el-option label="待处理" value="1" />
+              <el-option label="同意" value="2" />
+              <el-option label="退回" value="3" />
+              <el-option label="志愿编辑" value="4" />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+            <el-button
+              type="success"
+              plain
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['system:student-select-supervisor-record:export']"
+            >
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+            <el-button type="primary" @click="exportWordTemplate" :loading="exportLoading">批量导出互选表</el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+    
+      <ContentWrap v-if="userInfo.userType==='1' ||userInfo.userType==='4'" >
+        <el-table
+          v-loading="loading"
+          :data="selectionList"
+          row-key="id"
+          @selection-change="onRowSelectChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+            :selectable="selectableRows"
+          />
+          <el-table-column type="index" width="50" />
+            <el-table-column label="项目名称" align="center" prop="projectName" />
+          <el-table-column label="学生姓名" align="center" prop="studentName" />
+          <el-table-column label="硕士类型" align="center" prop="masterType" >
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.SYSTEM_STUDENT_MASTER_TYPE" :value="scope.row.masterType" />
+            </template>
+          </el-table-column>
+            <el-table-column label="导师姓名" align="center" prop="supervisor" />
+        
+            <el-table-column
+              label="导师类型"
+              align="center"
+              prop="supervisorType"
+              :formatter="userTypeFormatter"
+            />
+          <el-table-column label="申请状态" align="center" prop="selectType" >
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.SYSTEM_STUDENT_SELECT_RECORD_SELECT_TYPE" :value="scope.row.selectType" />
+            </template>
+          </el-table-column>
+          <el-table-column label="学生电子签名" align="center" prop="studentSignature">
+            <template #default="scope">
+              <img v-if="scope.row.studentSignature" :src="scope.row.studentSignature" style="width: 100px; height: 50px;" />
+            </template>
+          </el-table-column>
+          <el-table-column label="导师电子签名" align="center" prop="supervisorSignature">
+            <template #default="scope">
+              <img v-if="scope.row.supervisorSignature" :src="scope.row.supervisorSignature" style="width: 100px; height: 50px;" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="提交时间"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            width="180px"
+          />
+          <el-table-column
+            label="导师审批时间"
+            align="center"
+            prop="supervisorApproveTime"
+            :formatter="dateFormatter"
+            width="180px"
+          />
+          <el-table-column label="操作" align="center" min-width="120px"   v-if="userInfo.userType==='4'">
+            <template #default="scope">
+              <el-button
+                link
+                type="primary"
+                @click="openForm('update',  scope.row.projectId,scope.row.supervisorId,scope.row.id,scope.row.studentId)"
+                v-hasPermi="['system:student-select-supervisor-record:update']"
+              >
+                志愿编辑
+              </el-button>
+              <el-button
+                link
+                type="primary"
+                v-if="scope.row.selectType === 2"
+                @click="openForm('end',  scope.row.projectId,scope.row.supervisorId,scope.row.id,scope.row.studentId)"
+              >
+                详情
+              </el-button>
+            </template>
+          </el-table-column>
+      </el-table>
+      
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getRecordList"
       />
-    </el-table>
-  </ContentWrap>
+    </ContentWrap>
+  <!-- </el-loading> -->
+  <!-- 表单弹窗:添加/修改 -->
+  <studentSelectSupervisorRecordForm ref="formRef" @success="getRecordList" />
+
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue'
+import axios from 'axios';
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { supervisorSelectionSettingApi, supervisorSelectionSettingVO } from '@/api/system/supervisorSelectionSetting'
 import * as UserApi from '@/api/system/user'
 import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
-import {
-  studentSelectSupervisorRecordApi,
-  studentSelectSupervisorRecordVO
-} from "@/api/system/studentSelectSupervisorRecord";
+import {studentSelectSupervisorRecordApi, studentSelectSupervisorRecordVO} from "@/api/system/studentSelectSupervisorRecord";
 import {DICT_TYPE} from "@/utils/dict";
+import studentSelectSupervisorRecordForm from './studentSelectSupervisorRecordForm.vue'
+import {studentSelectionProjectApi} from "@/api/system/studentSelectionProject"
+import { exportDocx } from '@/utils/doc.js';
+// import docxtemplater from "docxtemplater"
+// import PizZip from "pizzip"
+// import JSZipUtils from "jszip-utils"
+// import JSZip from "jszip"
+// import { saveAs } from 'file-saver'
+// import { htmlPdf } from "@/utils/htmlToPDF.js"  
 
 /** 导师学硕专硕名额设置 列表 */
-defineOptions({ name: 'recordList' })
+defineOptions({ name: 'RecordList' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref<supervisorSelectionSettingVO[]>([]) // 列表的数据
-
+const isLoading = ref(false)
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   projectId: undefined,
   supervisorId: undefined,
-  academicSlots: undefined,
-  professionalSlots: undefined,
-  supervisorName:undefined,
+  externalSupervisorId:undefined,
+  supervisor:undefined,
   userType:undefined,
+  studentName:"",
   createTime: [],
+  projectName:"",
+  studentGrade:""
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
-
-//记录页面
-const router = useRouter()
-const openStudentSelectSupervisorRecord = (projectId,supervisorId) => {
-  router.push({ name: 'studentSelectSupervisorRecord', query: { projectId: projectId ,supervisorId :supervisorId} });
-}
-
 //导师类型
 const userTypeFormatter = (row, column, cellValue, index) => {
   switch (cellValue) {
@@ -114,7 +263,7 @@ const getUserInfo = async () => {
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
-  getList()
+  getRecordList()
 }
 
 /** 重置按钮操作 */
@@ -123,16 +272,155 @@ const resetQuery = () => {
   handleQuery()
 }
 
-//获取所有导师
-const supervisors = ref()
-const getSupervisor= async () => {
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, projectId?: number,supervisorId? :number,id?:number,studentId?:number) => {
+  formRef.value.open(type, projectId,supervisorId,id,studentId)
+}
+
+// 添加一个响应式的变量来保存选中的行
+const selectedRows = ref([]);
+const selectableRows = (row) => {
+  return row.selectType === 2;
+};
+// 处理复选框选择改变的函数
+const onRowSelectChange = (selected) => {
+  selectedRows.value = selected; // 直接将选中的行数组赋值给 selectedRows
+  // console.log('选中的行:', selectedRows.value);
+}
+
+watch(selectedRows, (newSelection) => {
+  console.log('选中的行数据:', newSelection);
+});
+
+/** 导出按钮操作 */
+const ibj = ref({
+  name: '示例名称',
+  conditionCons: [
+    {
+      groupName: '条件组 1',
+      cont: '条件内容 1'
+    },
+    {
+      groupName: '条件组 2',
+      cont: '条件内容 2'
+    }
+  ]
+});
+const exportWordTemplate = async () => {
+  const fileName = '导出文档.docx';
   try {
-    const result = await UserApi.getSupervisor()
-    supervisors.value = result
+    const response = await axios.get('/templates/templete.docx', {
+      responseType: 'blob' // 确保以 blob 形式获取文件
+    });
+    
+    // 使用你之前的 exportDocx 方法处理文件
+    exportDocx(URL.createObjectURL(response.data), ibj.value, fileName);
   } catch (error) {
-    console.error('未获取到导师', error)
+    console.error('导出文档失败:', error);
   }
-}
+};
+// const exportWordTemplate = async () => {
+//   if (selectedRows.value.length === 0) {
+//     message.error('请先选择需要导出的数据!');
+//     return;
+//   }
+//   // isLoading.value = true;
+//   // 遍历所有选中的行
+//   for (const row of selectedRows.value) {
+//     openForm('end', row.projectId, row.supervisorId, row.id, row.studentId);
+//     // 等待弹窗渲染完成
+//     await nextTick();
+//     // 调用弹窗中的导出方法
+//     if (formRef.value && typeof formRef.value.handleExport === 'function') {
+//       formRef.value.handleExport(); // 调用导出方法
+//     }
+//   }
+//   // isLoading.value = false;
+// };
+// const exportWordTemplate = async () => {
+//     const rows = selectedRows.value; 
+//     console.log("rows", rows);
+//     if (rows.length === 0) {
+//       alert('请先选择需要导出的数据!');
+//       return;
+//     }
+
+//     const ibj = rows.map((row) => ({
+//       name: "普通字段",
+//       conditionCons: [
+//         { 
+//           // "no": index + 1,
+//           groupName: row.projectName, 
+//           cont: row.studentName, 
+//           // "Column3": row.masterType, 请求 templete.docx 的项
+//           // "Column4": row.supervisor 
+//         },
+//       ]
+//     }));
+//     console.log("生成的数据对象:", ibj);
+//     const tempDocxpath = `${window.location.origin}/templete.docx?v=${new Date().getTime()}`;
+//     console.log(tempDocxpath);
+//     exportDocx(tempDocxpath, ibj, '模版.docx')
+// }
+// const exportWordTemplate = async () => {
+//   const rows = selectedRows.value; 
+//   console.log("rows", rows);
+//   if (rows.length === 0) {
+//     alert('请先选择需要导出的数据!');
+//     return;
+//   }
+
+//   const objList = rows.map((row, index) => ({
+//     name: "普通字段",
+//     conditionCons: [
+//       { 
+//         // "no": index + 1,
+//         groupName: row.projectName, 
+//         cont: row.studentName, 
+//         // "Column3": row.masterType, 
+//         // "Column4": row.supervisor 
+//       },
+//     ]
+//   }));
+
+//   const tempDocxpath = '/templete.docx';
+//   const zipName = 'word模板压缩包';
+
+//   if (objList.length > 1) {
+//     // 多组数据,生成ZIP文件
+//     const promises = objList.map((element, index) => {
+//       const fileName = `${index + 1}word模板.docx`;
+//       return JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
+//         if (error) throw error;
+//         const zip = new PizZip(content);
+//         let doc = new docxtemplater().loadZip(zip);
+//         doc.setData(element);
+//         doc.render();
+//         const out = doc.getZip().generate({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
+//         return { fileName, content: out };
+//       });
+//     });
+
+//     const zip = new JSZip();
+//     await Promise.all(promises).then(files => {
+//       files.forEach(file => zip.file(file.fileName, file.content, { binary: true }));
+//       const content = zip.generateAsync({ type: 'blob' });
+//       saveAs(content, zipName);
+//     });
+//   } else {
+//     // 单组数据,直接生成Word文件
+//     JSZipUtils.getBinaryContent(tempDocxpath, (error, content) => {
+//       if (error) throw error;
+//       const zip = new PizZip(content);
+//       let doc = new docxtemplater().loadZip(zip);
+//       doc.setData(objList[0]);
+//       doc.render();
+//       const out = doc.getZip().generate({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
+//       saveAs(out, zipName);
+//     });
+//   }
+// };
 
 const recordData = ref({
   id: undefined,
@@ -145,36 +433,64 @@ const recordData = ref({
   supervisorSignature: "",
   externalSupervisorId: undefined,
   masterType:undefined,
-  agreeDate:[]//同意日期
+  studentSignDate:[],
+  supervisorSignDate:[],
 })
-const handelWithdraw = async (supervisorId:number) => {
-  try {
-    await message.confirm()
-    recordData.value.supervisorId=supervisorId
-    const data =recordData.value as unknown as studentSelectSupervisorRecordVO
-    await studentSelectSupervisorRecordApi.withdrawStudentSelectSupervisorRecord(data)
-    message.success("撤回了申请")
-    await getList()
-  } catch {}
-}
 
 const selectionList = ref<studentSelectSupervisorRecordVO[]>([]) //下面的选择记录列表
 /** 查询互选记录列表 */
 const getRecordList = async () => {
   loading.value = true
   try {
-    const data = await studentSelectSupervisorRecordApi.getSelectSupervisorRecordList(queryParams)
+    const data = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecordPage(queryParams)
     selectionList.value = data
+    // console.log("selectionList", selectionList.value);
+    selectionList.value = data.list
+    total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
+const supervisors = ref()
+const getSupervisor= async () => {
+  try {
+    const result = await UserApi.getSupervisor()
+    supervisors.value = result
+  } catch (error) {
+    console.error('未获取到导师', error)
+  }
+}
+
+//传supervisorId给formData.supervisorId
+const handleSupervisorChange = () => {
+
+}
+
+const currentYear = new Date().getFullYear(); // 获取当前年份
+const gradeOptions = computed(() => {
+  return [
+    currentYear + '级',         // 今年
+    currentYear - 1 + '级',     // 去年
+    currentYear - 2 + '级',     // 前年
+    currentYear - 3 + '级',     // 大前年
+    currentYear - 4 + '级',     // 大大前年
+  ];
+});
+
+const projects = ref()
+const getAllProject= async () => {
+  try {
+    projects.value = await studentSelectionProjectApi.getAllProject()
+  } catch (error) {
+    console.error('未获取到项目', error)
+  }
+}
 /** 初始化 **/
 onMounted(() => {
-  getList()
   getUserInfo()
-  getSupervisor()
   getRecordList()
+  getAllProject()
+  getSupervisor()
 })
 </script>

+ 510 - 83
src/views/system/studentSelectSupervisorRecord/studentSelectSupervisorRecordForm.vue

@@ -1,57 +1,149 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog id="pdfRef" :title="dialogTitle" v-model="dialogVisible" class="mutual-selection-dialog">
+    <div class="header">
+      <h3>师生互选表</h3>
+    </div>
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="100px"
+      label-width="120px"
       v-loading="formLoading"
+      class="form-container"
+      @submit.prevent="handleFormSubmit"
     >
-      <el-row>
-        <el-col :span="12" >
-          <el-form-item label="导师姓名" prop="nickname">
-            <el-input v-model="formData.nickname" placeholder="请输入姓名" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="职称" prop="">
-            <el-input model-value="导师"  />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row >
-        <el-col :span="12">
-          <el-form-item label="" prop="major">
-            <el-input v-model="formData.major" placeholder="导师暂未填写" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="24">
-          <el-form-item label="对研究生毕业的成果要求" prop="studentAchievementRequirement">
-            <Editor v-model="formData.studentAchievementRequirement" ref="editorRef"/>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-<!--      &lt;!&ndash; 学生电子签名 &ndash;&gt;-->
-<!--      <el-form-item label="学生电子签名" prop="studentSignature">-->
-<!--        <vue-esign ref="studentSignaturePad" :width="400" :height="200" @save="handleStudentSignatureSave" />-->
-<!--      </el-form-item>-->
-
-<!--      &lt;!&ndash; 导师电子签名 &ndash;&gt;-->
-<!--      <el-form-item label="导师电子签名" prop="supervisorSignature">-->
-<!--        <vue-esign ref="supervisorSignaturePad" :width="400" :height="200" @save="handleSupervisorSignatureSave" />-->
-<!--      </el-form-item>-->
+      <!-- 学生信息 -->
+      <div class="section"  v-if="userInfo.userType==='4' ||userInfo.userType==='3'" >
+        <el-row>
+          <el-col :span="11">
+            <el-form-item label="学生姓名">
+              <el-input v-model="studentData.nickname" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="13">
+            <el-form-item label="学号">
+              <el-input v-model="studentData.userNumber" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="11">
+            <el-form-item label="专业">
+              <el-input v-model="studentData.major" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="13">
+            <el-form-item label="联系电话">
+              <el-input v-model="studentData.mobile" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
 
+      <!-- 学生志愿 -->
+      <div class="section" v-if="userInfo.userType !== '4' || formType === 'end'">
+        <h4>学生志愿</h4>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="导师姓名">
+              <el-input v-model="supervisorData.nickname" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="校内导师">
+              <el-input v-model="supervisorData.title" placeholder="自动链接" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="对研究生毕业时学术成果的要求">
+              <el-input type="textarea" v-model="supervisorData.studentAchievementRequirement" placeholder="(未填写时,默认学校及学院发表学术成果的需求执行)" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="志愿填报">
+              <p>本人已知晓{{supervisorData.nickname}}对研究生毕业时学术成果的发表要求,特申请加入{{supervisorData.nickname}}课题组进行研究生阶段的学习和研究。</p>
+              <p v-if="userInfo.userType=='1'">★ 本人签字
+                <!-- <el-input v-model="formData.studentSignature" placeholder="" /> -->
+              </p>
+              <div v-if="userInfo.userType=='1'">
+                <canvas 
+                  id="canvas" 
+                  ref="canvas" 
+                  width="300px" 
+                  height="100px" 
+                  @mousedown="handleMouseDown"
+                  @mousemove="handleMouseMove"
+                  @mouseup="handleMouseUp"
+                  style="border:1px solid #e0e0e0; border-radius: 4px; background-color: white;"
+                ></canvas>
+                <div style="display: flex; justify-content: flex-end;">
+                  <button type="button" @click="handleStudentSignatureSave" style="padding: 2px 5px; margin-right: 10px;">保存</button>
+                  <button type="button" @click="ClearStudentSignature" style="padding: 2px 5px;">清空</button>
+                </div>
+              </div>
+              <p v-if="userInfo.userType==='3' || formType === 'end'">★ 本人签字</p>
+              <img v-if="userInfo.userType==='3' || formType === 'end'" :src="formData.studentSignature" alt="学生签名" style="width: 300px; height: 100px; margin-right: 10px; background-color: white;"/>
+              <p v-if="userInfo.userType==='3' || formType === 'end'">日期:{{formattedStudentSignDate}}</p>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <!-- 导师意见 -->
+      <div class="section" v-if="userInfo.userType==='3'">
+        <p style="margin-bottom: 10px;">导师意见:同意</p>
+        <p style="margin-bottom: 10px;">导师签字</p>
+        <div>
+          <canvas 
+            id="canvas" 
+            ref="canvas" 
+            width="400px" 
+            height="100px" 
+            @mousedown="handleMouseDown"
+            @mousemove="handleMouseMove"
+            @mouseup="handleMouseUp"
+            style="border:1px solid #e0e0e0; border-radius: 4px; background-color: white;"
+          ></canvas>
+          <div style="display: flex; justify-content: flex-start; margin-top: 10px;">
+            <button type="button" @click="handleSupervisorSignatureSave" style="padding: 2px 5px; margin-right: 10px;">保存</button>
+            <button type="button" @click="ClearSupervisorSignature" style="padding: 2px 5px;">清空</button>
+          </div>
+        </div>
+        <!-- <p v-if="userInfo.userType==='4'">日期:{{formData.supervisorSignDate}}</p> -->
+      </div>
+      <div class="section" v-if="formType === 'end'">
+        <p style="font-weight: bold;">★ 导师签字</p>
+        <img :src="formData.supervisorSignature" alt="导师签名" style="width: 300px; height: 100px; margin-right: 10px; background-color: white;"/>
+        <p>日期:{{formattedSupervisorSignDate}}</p>
+      </div>
+      <div class="section" v-if="userInfo.userType==='4' && formType === 'update'">
+        <el-form-item label="参与导师" prop="supervisorId">
+          <el-select
+            v-model="supervisors.supervisorId"
+            placeholder="请选择参与导师"
+            clearable
+            filterable
+            class="!w"
+            @change="handleSupervisorChange"
+          >
+            <el-option
+              v-for="supervisor in supervisors"
+              :key="supervisor.id"
+              :label="supervisor.nickname"
+              :value="supervisor.id"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
+      <div style="display: flex; justify-content: center;">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="formType !== 'end'">{{ submitFormText }}</el-button>
+        <el-button @click="dialogVisible = false" v-if="formType !== 'end'">取消</el-button>
+        <el-button @click="handleExport" type="primary" :disabled="formLoading" v-if="showPrintButton">打印</el-button>
+      </div>
     </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-      <button @click="saveSupervisorSignature">保存签名</button>
-    </template>
   </Dialog>
 </template>
 
@@ -59,8 +151,11 @@
 import { studentSelectSupervisorRecordApi, studentSelectSupervisorRecordVO } from '@/api/system/studentSelectSupervisorRecord'
 import { CommonStatusEnum } from '@/utils/constants'
 import * as UserApi from '@/api/system/user'
-import { defineOptions, defineExpose, ref, reactive } from 'vue'
-import {data} from "autoprefixer";
+import { defineOptions, defineExpose, ref, reactive, nextTick, onMounted, computed } from 'vue'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import {supervisorSelectionSettingApi} from '@/api/system/supervisorSelectionSetting'
+import { htmlPdf } from "@/utils/htmlToPDF.js"  
+
 
 /** 师生互选记录 表单 */
 defineOptions({ name: 'studentSelectSupervisorRecordForm' })
@@ -70,6 +165,7 @@ const message = useMessage() // 消息弹窗
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
+const submitFormText = ref('') // 提交按钮的文字
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
@@ -82,12 +178,45 @@ const formData = ref({
   supervisorApproveTime: undefined,
   supervisorSignature: "",
   externalSupervisorId: undefined,
+  studentSignDate:[],
+  supervisorSignDate:[],
+})
+
+const formattedStudentSignDate = computed(() => {
+  if (formData.value.studentSignDate) {
+    const year = formData.value.studentSignDate[0];
+    const month = formData.value.studentSignDate[1];
+    const day = formData.value.studentSignDate[2];
+    return `${year}.${month}.${day}`;
+  }
+  return '';
+});
+
+const formattedSupervisorSignDate = computed(() => {
+  if (formData.value.supervisorSignDate) {
+    const year = formData.value.supervisorSignDate[0];
+    const month = formData.value.supervisorSignDate[1];
+    const day = formData.value.supervisorSignDate[2];
+    return `${year}.${month}.${day}`;
+  }
+  return '';
+});
 
-  //导师的
+const supervisorData = ref({
+  //导师的信息
   nickname: '',
   remark: ' ',
   major:'',
   studentAchievementRequirement:"",
+  title:""
+})
+
+const studentData = ref({
+  //导师的信息
+  nickname: "",
+  userNumber:"",
+  major:"",
+  mobile:"",
 })
 const formRules = reactive({
   // projectId: [{ required: true, message: '项目id不能为空', trigger: 'blur' }],
@@ -96,63 +225,283 @@ const formRules = reactive({
 const supervisorSignaturePad = ref(null);
 const formRef = ref() // 表单 Ref
 
+//获取登录人员信息
+const userInfo = ref({} as ProfileVO)
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+}
+
+// 获取所有导师
+const supervisors = ref()
+const getSupervisor= async () => {
+  try {
+    const result = await UserApi.getSupervisor()
+    // console.log('getSupervisor',result);
+    supervisors.value = result
+  } catch (error) {
+    console.error('未获取到导师', error)
+  }
+}
+
+const showPrintButton = ref(false); // 控制打印按钮的显示
+
 /** 打开弹窗 */
+const open = async (type: string, projectId?: number ,supervisorId?:number, id?:number,studentId?:number) => {
+  dialogVisible.value = true;
+  if (type === 'create'){
+    dialogTitle.value = '志愿填报'
+    submitFormText.value = '选择'
+    formType.value = type
+    resetForm()
+    if (supervisorId) {
+      formLoading.value = true
+      try {
+        console.log(supervisorId)
+        console.log(projectId)
+        const supervisor = await supervisorSelectionSettingApi.getSupervisorInfo(supervisorId,projectId);
+        //需要传的
+        formData.value.supervisorId =supervisorId;
+        formData.value.projectId = projectId;
+        //显示的
+        supervisorData.value.nickname = supervisor.supervisorName;
+        supervisorData.value.title =supervisor.title;
+        supervisorData.value.studentAchievementRequirement = supervisor.studentAchievementRequirement;
+        supervisorData.value.major = supervisor.major;
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  if (type === 'agree'){
+    dialogTitle.value = '通过志愿'
+    submitFormText.value = '接受'
+    formType.value = type
+    resetForm()
+    if (studentId) {
+      formLoading.value = true
+      try {
+        const result =await UserApi.getUser(studentId)
+        studentData.value.nickname=result.nickname
+        studentData.value.userNumber=result.userNumber
+        studentData.value.major=result.major
+        studentData.value.mobile=result.mobile
 
-const open = async (type: string, projectId?: number ,supervisorId?:number) => {
-  dialogVisible.value = true
-  dialogTitle.value = "志愿填报"
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (supervisorId) {
-    formLoading.value = true
-    try {
-      console.log(supervisorId)
-      const user = await UserApi.getUser(supervisorId);
-      // formData.value = await studentSelectSupervisorRecordApi.getstudentSelectSupervisorRecord(id)
-      console.log(user)
-      formData.value.supervisorId =supervisorId;
-      formData.value.projectId = projectId;
-      formData.value.nickname = user.nickname;
-      formData.value.studentAchievementRequirement = user.studentAchievementRequirement;
-      formData.value.major = user.major;
-    } finally {
-      formLoading.value = false
+        const supervisor = await supervisorSelectionSettingApi.getSupervisorInfo(supervisorId,projectId);
+        //显示的
+        supervisorData.value.nickname = supervisor.supervisorName;
+        supervisorData.value.title =supervisor.title;
+        supervisorData.value.studentAchievementRequirement = supervisor.studentAchievementRequirement;
+        supervisorData.value.major = supervisor.major;
+        formData.value = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecord(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  if (type === 'update'){
+    dialogTitle.value = '志愿编辑'
+    submitFormText.value = '编辑'
+    formType.value = type
+    resetForm()
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecord(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+    if (supervisorId) {
+      formLoading.value = true
+      try {
+        console.log(supervisorId)
+        console.log(projectId)
+        const supervisor = await supervisorSelectionSettingApi.getSupervisorInfo(supervisorId,projectId);
+        //需要传的
+        formData.value.supervisorId =supervisorId;
+        formData.value.projectId = projectId;
+        //显示的
+        supervisorData.value.nickname = supervisor.supervisorName;
+        supervisorData.value.title =supervisor.title;
+        supervisorData.value.studentAchievementRequirement = supervisor.studentAchievementRequirement;
+        supervisorData.value.major = supervisor.major;
+      } finally {
+        formLoading.value = false
+      }
     }
+    if (studentId) {
+      formLoading.value = true
+      try {
+        const result =await UserApi.getUser(studentId)
+        studentData.value.nickname=result.nickname
+        studentData.value.userNumber=result.userNumber
+        studentData.value.major=result.major
+        studentData.value.mobile=result.mobile
+
+        const supervisor = await supervisorSelectionSettingApi.getSupervisorInfo(supervisorId,projectId);
+        //显示的
+        supervisorData.value.nickname = supervisor.supervisorName;
+        supervisorData.value.title =supervisor.title;
+        supervisorData.value.studentAchievementRequirement = supervisor.studentAchievementRequirement;
+        supervisorData.value.major = supervisor.major;
+        formData.value = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecord(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  if (type === 'end'){
+    dialogTitle.value = '详情'
+    submitFormText.value = '编辑'
+    formType.value = type
+    resetForm()
+    if (id) {
+      formLoading.value = true
+      try {
+        formData.value = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecord(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+    if (studentId) {
+      formLoading.value = true
+      try {
+        const result =await UserApi.getUser(studentId)
+        studentData.value.nickname=result.nickname
+        studentData.value.userNumber=result.userNumber
+        studentData.value.major=result.major
+        studentData.value.mobile=result.mobile
+
+        const supervisor = await supervisorSelectionSettingApi.getSupervisorInfo(supervisorId,projectId);
+        //显示的
+        supervisorData.value.nickname = supervisor.supervisorName;
+        supervisorData.value.title =supervisor.title;
+        supervisorData.value.studentAchievementRequirement = supervisor.studentAchievementRequirement;
+        supervisorData.value.major = supervisor.major;
+        formData.value = await studentSelectSupervisorRecordApi.getStudentSelectSupervisorRecord(id)
+      } finally {
+        formLoading.value = false
+      }
+    }
+  }
+  await nextTick();
+  if (canvas.value) {
+    context.value = canvas.value.getContext('2d');
+    context.value.lineWidth = 2; // 设置线条宽度
+    context.value.lineCap = 'round'; // 设置线条末端样式
+    context.value.strokeStyle = 'black'; // 设置线条颜色
+  } else {
+    console.error('Canvas element is not found');
+  }
+}
+
+const handleExport = (row) => {
+  var fileName = '师生互选表';
+  const formElement = document.querySelector('.form-container'); 
+  htmlPdf(fileName, formElement, row);
+  dialogVisible.value = false;
+};
+defineExpose({ open, handleExport });
+
+const canvas = ref() // 签名画布
+const isDrawing = ref(false) // 是否正在画
+const context = ref() // 签名画布的上下文
+
+const handleMouseDown = (event: MouseEvent) => {
+  if (!canvas.value || !context.value) {
+    console.warn('Canvas or context is not initialized yet');
+    return;
+  }
+  isDrawing.value = true;
+  const { offsetX, offsetY } = event;
+  context.value.beginPath();
+  context.value.moveTo(offsetX, offsetY);
+}
+
+const handleMouseMove = (event: MouseEvent) => {
+  if (!isDrawing.value || !canvas.value || !context.value) return;
+  const { offsetX, offsetY } = event;
+  context.value.lineTo(offsetX, offsetY);
+  context.value.stroke();
+}
+
+const handleMouseUp = () => {
+  isDrawing.value = false
+}
+
+const ClearStudentSignature = () => {
+  if (context.value) {
+    formData.value.studentSignature = "";
+    context.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
+    message.success('签名已清空');
+  } else {
+    console.warn('Canvas context is not initialized yet');
   }
 }
 
-const handleStudentSignatureSave = (data: string) => {
-  console.log("Student Signature Data:", data); // 调试输出签名数据
-  formData.value.studentSignature = data;  // 保存学生签名数据
+const handleStudentSignatureSave = () => {
+  if (canvas.value) {
+    const dataURL = canvas.value.toDataURL();
+    console.log(dataURL);
+    formData.value.studentSignature = dataURL; // 保存学生签名数据
+    message.success('学生签名保存成功');
+  } else {
+    console.warn('Canvas element is not initialized yet');
+  }
 };
 
-const handleSupervisorSignatureSave = (data: string) => {
-  console.log("Supervisor Signature Data:", data); // 调试输出签名数据
-  formData.value.supervisorSignature = data;  // 保存导师签名数据
+const ClearSupervisorSignature = () => {
+  if (context.value) {
+    formData.value.supervisorSignature = "";
+    context.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
+    message.success('签名已清空');
+  } else {
+    console.warn('Canvas context is not initialized yet');
+  }
+}
+
+const handleSupervisorSignatureSave = () => {
+  if (canvas.value) {
+    const dataURL = canvas.value.toDataURL();
+    console.log(dataURL);
+    formData.value.supervisorSignature = dataURL; // 保存学生签名数据
+    message.success('导师签名保存成功');
+  } else {
+    console.warn('Canvas element is not initialized yet');
+  }
 };
 
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+// defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
-  await formRef.value.validate();
-
-  // 在提交前检查签名数据
-  // if (!formData.value.studentSignature) {
-  //   message.warning("请签署学生电子签名!");
-  //   return;
-  // }
   formLoading.value = true;
   try {
+    const isStudentSignatureSaved = formData.value.studentSignature.startsWith('data:image/png;base64,');
+    const isSupervisorSignatureSaved = formData.value.supervisorSignature.startsWith('data:image/png;base64,');
+    if (!isStudentSignatureSaved && (userInfo.value.userType === '1')) {
+      message.error('请先保存签名');
+      formLoading.value = false;
+      return;
+    }
+    if (!isSupervisorSignatureSaved && (userInfo.value.userType === '3')) {
+      message.error('请先保存签名');
+      formLoading.value = false;
+      return;
+    }
     const data = formData.value as unknown as studentSelectSupervisorRecordVO;
+    console.log(data)
     if (formType.value === 'create') {
       await studentSelectSupervisorRecordApi.createStudentSelectSupervisorRecord(data);
       message.success(t('common.createSuccess'));
-    } else {
+    } else if (formType.value === 'update') {
       await studentSelectSupervisorRecordApi.updateStudentSelectSupervisorRecord(data);
       message.success(t('common.updateSuccess'));
+    } else if (formType.value === 'agree') {
+      await studentSelectSupervisorRecordApi.agreeStudentSelectSupervisorRecord(data);
+      message.success("已经同意申请");
     }
     dialogVisible.value = false;
     emit('success');
@@ -161,6 +510,16 @@ const submitForm = async () => {
   }
 };
 
+const handleFormSubmit = () => {
+  console.log('Form submit prevented');
+};
+
+const isSupervisor = computed(() => userInfo.value.userType === '3');
+
+// 直接改导师(志愿编辑)
+const handleSupervisorChange = (supervisorId?:number) => {
+  supervisorId = supervisors.value.id;
+}
 
 /** 重置表单 */
 const resetForm = () => {
@@ -177,5 +536,73 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
+
+/** 初始化 **/
+onMounted(() => {
+  getSupervisor()
+  getUserInfo()
+})
 </script>
 
+<style scoped>
+.mutual-selection-dialog {
+  width: 600px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.header {
+  background-color: #f5f5f5;
+  padding: 10px;
+  text-align: center;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.form-container {
+  padding: 20px;
+}
+
+.section {
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: #fcfcfc;
+}
+
+.section h4 {
+  margin-top: 0;
+  color: #333;
+}
+
+.el-form-item {
+  margin-bottom: 20px;
+}
+
+.el-input {
+  width: 100%;
+}
+
+.el-button {
+  margin-right: 10px;
+}
+
+.el-button[type="primary"] {
+  background-color: #409EFF;
+  border-color: #409EFF;
+}
+
+.el-button[type="primary"]:hover {
+  background-color: #66b1ff;
+  border-color: #66b1ff;
+}
+
+@media print {
+  .header {
+    display: none; /* 打印时隐藏标题 */
+  }
+
+  /* 可以根据需要隐藏其他不需要的部分 */
+}
+
+</style>

+ 7 - 8
src/views/system/studentSelectionProject/index.vue

@@ -5,7 +5,7 @@
     :inline="true"
     label-width="200px"
   >
-    <el-form-item label="是否开启导师弹窗"  v-if="userInfo?.userType === '4' || userInfo?.userType ==null ">
+    <el-form-item label="是否开启导师弹窗"  v-if="userInfo?.userType === '4' ">
       <el-switch v-model="popData.isSupervisor" :active-value="1" :inactive-value="0" @change="(val) => handleSwitchChange(popData.id, 'isSupervisor', val)" />
     </el-form-item>
 
@@ -320,20 +320,19 @@ const getPopDataFunction = async () => {
   try {
     popData.value = await getPopData();
     isPopDataLoaded.value = true; // 更新标志
-    console.log(popData.value)
   } finally {
     loading.value = false;
   }
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
-  getUserInfo()
-  if (userInfo.value.userType==="4") {
-    getPopDataFunction()
+onMounted(async () => {
+  getList();
+  await getUserInfo(); // 等待用户信息加载完成
+  if (userInfo.value.userType === '4') {
+    getPopDataFunction();
   }
-})
+});
 </script>
 
 <style>

+ 1 - 1
src/views/system/studentSelf/SForm.vue

@@ -41,7 +41,7 @@
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="用户类型" prop="major">
+          <el-form-item label="专业" prop="major">
             <el-select v-model="formData.major" placeholder="请选择专业">
              <el-option
                 v-for="option in majors"

+ 52 - 88
src/views/system/studentSelf/index.vue

@@ -71,65 +71,66 @@
         @update:visible="dialogVisible = $event"
         @success="handleSuccess"
       />
+    />
   </div>
+  <student ref="studentFormRef" @success="getUserInfo"/>
  
 </template>
 
-<script lang="ts">
-import { defineComponent, reactive, ref, onMounted } from 'vue';
+<script setup lang="ts">
+import { ref, onMounted, nextTick, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useMessage } from '@/hooks/web/useMessage';
 import SForm from './SForm.vue';
-import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import student from '../userDetail/student.vue';
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile';
+import { useRouter } from 'vue-router';
+import {PopDo,getIsPop} from '@/api/system/user/pop';
 
+const { t } = useI18n();
+const message = useMessage();
+const router = useRouter();
 
-export default defineComponent({
-  components: {
-    SForm,
-  },
-  setup() {
-    const { t } = useI18n();
-    const message = useMessage();
-    const dialogVisible = ref(false);
-    const loading = ref(true) // 列表的加载中
-    const formRef = ref();
+const dialogVisible = ref(false);
+const loading = ref(true);
+const formRef = ref();
+
+const studentFormRef = ref();
+const isPop  = ref();
+const userInfo = ref({} as ProfileVO);
+
+const extractedImageUrls = ref<string[]>([]);
+const cleanedremark = computed(() => {
+  return userInfo.value.remark ? userInfo.value.remark.replace(/<\/?[^>]+(>|$)/g, '') : '';
+});
 
-    // 将 userInfo 定义放入 setup 中
-const userInfo = ref({} as ProfileVO)
 const getUserInfo = async () => {
   try {
-    loading.value = true; 
+    loading.value = true;
     const users = await getUserProfile();
-    console.log(users, 'users');
     userInfo.value = users;
   } catch (error) {
     console.error('获取用户信息失败:', error);
   } finally {
-    loading.value = false; // 隐藏加载中
+    loading.value = false;
   }
-}
-
-const userTypeMapping = {
-  '1': '学生',
 };
 
 const openDialog = () => {
-  loading.value = true; // 显示加载中
   dialogVisible.value = true;
-  formRef.value.open(); // 打开弹窗
-  loading.value = false; // 隐藏加载中
+  formRef.value.open();
 };
 
-const extractedImageUrls = ref<string[]>([]);
 const handleSuccess = async (urls) => {
   extractedImageUrls.value = urls;
-  console.log('提取到的图片URL:', extractedImageUrls);
+  console.log('提取到的图片URL:', extractedImageUrls.value);
   dialogVisible.value = false;
   await getUserInfo();
 };
+
 const fetchImageUrls = async () => {
   try {
-    loading.value = true; // 开始加载
+    loading.value = true;
     const res = await getUserProfile();
     const urls = res.remark.match(/<img.*?src="(.*?)"/g);
     if (urls) {
@@ -143,74 +144,37 @@ const fetchImageUrls = async () => {
   } catch (error) {
     console.error('获取图片 URL 失败:', error);
   } finally {
-    loading.value = false; // 完成加载
+    loading.value = false;
   }
 };
-// 创建一个 computed 属性来处理并去掉 <p> 标签
-  const cleanedremark = computed(() => {
-  return userInfo.value.remark ? userInfo.value.remark.replace(/<\/?[^>]+(>|$)/g, '') : '';
-});
 
+const getPopDataFunction =(async ()=>{
+  isPop.value = await  getIsPop();
+  console.log(isPop.value)
+})
 
-    // 表单提交
-    // const submit = async () => {
-    //   try {
-    //     await formRef.value?.validate();
-    //     console.log('提交的数据:', form);
-    //     await updateDept(form).then((res) => {
-    //       console.log('更新成功:', res);
-    //     });
-    //     message.success('成功');
-    //   } catch (error) {
-    //     console.error('提交错误:', error);
-    //     message.error('错误');
-    //   }
-    // };
-
-
-    // 表单重置
-    // const init = async () => {
-    //   const res = await getUserProfile();
-    //   console.log('获取的数据:', res);
-    //   form.id = res.id;
-    //   form.address = res.address;
-    //   form.supervisor = res.user.nickname;
-    //   form.phone = res.phone;
-    //   form.email = res.email;
-    //   form.name = res.name;
-    //   form.leaderUserId = res.user.id;
-    //   form.remark = res.remark;
-    //   userInfo.value = res.user; 
-    // };
-
-    onMounted(async () => {
-      // await init();
-      await getUserInfo();
-      await fetchImageUrls(); // 这里 add 一個方法来加载图片
-    });
+const studentForm = () => {
+  if (studentFormRef.value) {
+    studentFormRef.value.open();
+  } else {
+    console.error('Student form component is not mounted yet!');
+  }
+};
 
-    return {
-      t,
-      // form,
-      userInfo,   
-      // init,
-      loading,
-      getUserInfo,
-      fetchImageUrls,
-      formRef,
-      dialogVisible,
-      openDialog,
-      handleSuccess,
-      extractedImageUrls,
-      cleanedremark,
-      userTypeMapping,
-    };
+onMounted(async () => {
+  await getUserInfo();
+  await fetchImageUrls();
+  if (userInfo.value.userType === '1') {
+    await getPopDataFunction();
+    if (isPop.value) {
+      await nextTick();
+      studentForm();
+    }
   }
 });
-
-
 </script>
 
+
 <style scoped>
 .user-info {
   margin-top: 10px;
@@ -297,4 +261,4 @@ const fetchImageUrls = async () => {
   height: auto; /* 保持高度自动 */
 }
 
-</style>
+</style>

+ 13 - 12
src/views/system/supervisorSelectionSetting/index.vue

@@ -12,7 +12,6 @@
       <el-form-item label="参与导师" prop="supervisorIds">
         <el-select
           v-model="queryParams.supervisorId"
-          @change="handleSupervisorChange"
           placeholder="请选择参与导师"
           clearable
           filterable
@@ -120,9 +119,9 @@
         </template>
       </el-table-column>
       <el-table-column
-        label="创建时间"
+        label="更新时间"
         align="center"
-        prop="createTime"
+        prop="updateTime"
         :formatter="dateFormatter"
         width="180px"
       />
@@ -139,13 +138,14 @@
           </el-button>
         </template>
       </el-table-column>
+
       <!--  学生志愿填报-->
       <el-table-column label="志愿填报" align="center" min-width="120px"  v-if=" userInfo?.userType === '1'">
         <template #default="scope">
           <el-button
             type="primary"
             link
-            @click="openStudentSelectSupervisorPop('create', scope.row.projectId,scope.row.supervisorId)"
+            @click="openStudentSelectSupervisorPop('create', scope.row.projectId,scope.row.supervisorId,null,null)"
             v-hasPermi="['system:student-select-supervisor-record:create']"
             v-if="userInfo?.userType === '1'"
           >
@@ -155,6 +155,7 @@
         </template>
       </el-table-column>
 
+
       <el-table-column label="操作" align="center" min-width="120px">
         <template #default="scope">
 <!--          学院查看招生详情-->
@@ -396,18 +397,17 @@ const getSupervisor= async () => {
     console.error('未获取到导师', error)
   }
 }
-
+//传supervisorId给formData.supervisorId
 
 const teacherRequireFormRef = ref()
 const openTeacherRequireForm = (supervisorId?: number) => {
-  teacherRequireFormRef.value.open(supervisorId)
+  teacherRequireFormRef.value.open("detail",supervisorId)
 }
 
 //志愿填报弹窗
 const studentSelectSupervisorPop= ref()
-const openStudentSelectSupervisorPop =  (type: string, projectId?: number,supervisorId? :number) => {
-  console.log(supervisorId)
-  studentSelectSupervisorPop.value.open(type, projectId ,supervisorId)
+const openStudentSelectSupervisorPop =  (type: string, projectId?: number,supervisorId? :number,id:number,studentId:number) => {
+  studentSelectSupervisorPop.value.open(type, projectId ,supervisorId,id,studentId)
 }
 
 //没处理好
@@ -423,7 +423,8 @@ const recordData = ref({
   supervisorSignature: "",
   externalSupervisorId: undefined,
   masterType:undefined,
-  agreeDate:[]//同意日期
+  studentSignDate:[],
+  supervisorSignDate:[],
 })
 const handelWithdraw = async (supervisorId:number) => {
   try {
@@ -449,10 +450,10 @@ const getRecordList = async () => {
 }
 
 /** 初始化 **/
-onMounted(() => {
+onMounted(async () => {
   getList()
   getUserInfo()
   getSupervisor()
-  getRecordList()
+  await getRecordList()
 })
 </script>

+ 192 - 0
src/views/system/userDetail/student.vue

@@ -0,0 +1,192 @@
+
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      class="custom-form"
+    >
+    <el-row >
+      <el-col :span="12">
+        <el-form-item label="姓名" prop="nickname" >
+          <el-input v-model="formData.nickname" placeholder="自动链接" :disabled=isStudent />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="学号" prop="userNumber">
+          <el-input v-model="formData.userNumber" placeholder="" :disabled=isStudent />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row >
+      <el-col :span="12">
+        <el-form-item label="专业" prop="major">
+          <el-input v-model="formData.major" placeholder="" :disabled=isStudent />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="联系电话" prop="mobile">
+          <el-input v-model="formData.mobile" placeholder="请输入联系电话"/>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row>
+      <el-col :span="24">
+        <el-form-item label="个人简历上传(PDF)" prop="introduction">
+          <upload-file v-model="formData.introduction" />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm" v-if="userInfo.userType==='1'">确 定</el-button>
+      <el-button @click="dialogVisible = false" v-if="userInfo.userType==='1'" >取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as UserApi from '@/api/system/user'
+import { FormRules } from 'element-plus'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import {ref} from "vue";
+
+import {selectionBookVO,selectionBookApi} from '@/api/system/studentSelectSupervisorRecord/selectionBook'
+
+defineOptions({ name: 'studentForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  sex: undefined,
+  nickname: '',
+  userNumber: '',//学号
+  major:'',//专业
+  mobile:"",
+  introduction:"",//简介PDF
+})
+const formRules = reactive<FormRules>({
+  mobile: [{ required: true, message: '联系电话不能为空', trigger: 'blur' }],
+  introduction: [{ required: true, message: '个人简历不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+const deptList = ref<Tree[]>([]) // 树形结构
+
+//获取登录人员信息
+const userInfo = ref({} as ProfileVO)
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+}
+const isStudent = computed(() => userInfo.value.userType === "1");
+
+  const open = async () => {
+  dialogVisible.value = true
+  dialogTitle.value = '信息填写'
+  formType.value = '' //
+  resetForm()
+  // 获取导师信息
+    formLoading.value = true
+    try {
+      const user = await UserApi.getUser(userInfo.value.id)
+      formData.value = user;
+    } finally {
+      formLoading.value = false
+    }
+
+}
+
+  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  formLoading.value = true
+  try {
+      const data = formData.value as unknown as selectionBookVO
+      if (Array.isArray(data.introduction) && data.introduction.length > 0) {
+      data.introduction = data.introduction[0]; // 只取数组的第一个元素
+      }
+      await selectionBookApi.updateSelectionBook(data)
+      console.log(data)
+      message.success("填写互选数据成功")
+      dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    sex: undefined,
+    nickname: '',
+    userNumber: '',//学号
+    major:'',//专业
+    mobile:"",
+    introduction:"",//简介PDF
+  }
+  formRef.value?.resetFields()
+}
+onMounted(() => {
+   getUserInfo()
+
+})
+
+</script>
+
+<style scoped>
+.custom-form {
+  background-color: #f0f4f8; /* 浅灰色背景 */
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
+}
+.custom-form .el-form-item {
+  margin-bottom: 20px;
+}
+.custom-form .el-input,
+.custom-form .el-textarea {
+  width: 100%;
+  border-color: #d9d9d9; /* 输入框边框颜色 */
+}
+.custom-form .el-form-item__label {
+  color: #333; /* 标签文字颜色 */
+}
+.custom-form .el-input__inner,
+.custom-form .el-textarea__inner {
+  background-color: #fff; /* 输入框背景颜色 */
+  color: #333; /* 输入框文字颜色 */
+}
+.custom-form .el-button {
+  background-color: #6a1b9a; /* 按钮背景颜色 */
+  border-color: #6a1b9a;
+  color: #fff; /* 按钮文字颜色 */
+}
+.custom-form .el-button--primary {
+  background-color: #4a148c; /* 主要按钮背景颜色 */
+  border-color: #4a148c;
+}
+.custom-form .el-button:hover,
+.custom-form .el-button--primary:hover {
+  background-color: #5a0073; /* 按钮悬浮背景颜色 */
+  border-color: #5a0073;
+}
+</style>

+ 163 - 96
src/views/system/userDetail/teacher.vue

@@ -1,3 +1,4 @@
+
 <template>
   <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
@@ -5,100 +6,116 @@
       v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="85px"
+      label-width="120px"
+      class="custom-form"
     >
-      <el-row>
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="姓名" prop="nickname">
-            <el-input v-model="formData.nickname" placeholder="请输入姓名" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="职称" prop="">
-            <el-input model-value="导师"  />
-          </el-form-item>
-        </el-col>
-      </el-row>
+    <el-row>
+      <el-col :span="12">
+        <el-form-item label="姓名" prop="nickname" >
+          <el-input v-model="formData.nickname" placeholder="自动链接" :disabled="!isSupervisor" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="职称" prop="title">
+          <el-input v-model="formData.title" placeholder="" :disabled="!isSupervisor"/>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row>
+      <el-col :span="24" >
+        <el-form-item label="外聘导师" style="font-weight: 550;">
+          <p></p>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" >
+        <el-form-item label="工作单位" prop="externalSupervisorWorkPlace">
+          <el-input v-model="formData.externalSupervisorWorkPlace" placeholder="请输入工作单位" :disabled="!isSupervisor"/>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row  >
+      <el-col :span="24">
+        <el-form-item label="合作的校内导师" prop="supervisor">
+          <el-input v-model="formData.supervisor" placeholder="自动链接" :disabled="!isSupervisor"/>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row  >
+      <el-col :span="24">
+        <el-form-item label="研究方向" prop="major">
+          <el-input v-model="formData.major" placeholder="请输入研究方向" :disabled="!isSupervisor" />
+        </el-form-item>
+      </el-col>
+    </el-row>
       <el-row >
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="" prop="major">
-             <el-input v-model="formData.major" placeholder="请输入研究方向" />
+        <!--        导师上传-->
+        <el-col :span="24" v-if="userInfo.userType==='3'&&formType==='update'">
+          <el-form-item label="个人简历上传" prop="introduction">
+            <!-- <upload-file v-model="formData.introduction" /> -->
+            <Editor v-model="formData.introduction" ref="editorRef" class="full-width-editor"/>
           </el-form-item>
         </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="用户性别" prop="sex">
-            <el-select v-model="formData.sex" placeholder="请选择">
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-
-        <el-col :span="12" v-if="formType === 'detail'">
-          <el-form-item label="工号" prop="userNumber">
-            <el-input v-model="formData.userNumber" placeholder="请输入工号" />
+        <!--        学院和学生下载-->
+        <el-col :span="24" v-if="userInfo.userType==='4'||userInfo.userType==='1'&&formType==='detail'">
+          <el-form-item label="导师简历下载(PDF)" prop="introduction">
+              <el-button
+                v-if="formData.introduction"
+                type="text"
+                icon="el-icon-download"
+                @click="handleDownload"
+                class="custom-download-button"
+              >
+                下载导师简历
+              </el-button>
+              <span v-else>暂无文件</span>
           </el-form-item>
         </el-col>
       </el-row>
-
-      <el-row>
-        <el-col :span="24" v-if="formType === 'detail'">
-          <el-form-item label="个人简介" prop="remark" >
-            <Editor v-model="formData.remark" ref="editorRef"/>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="24" v-if="formType === 'detail'">
-          <el-form-item label="对研究生毕业的成果要求" prop="studentAchievementRequirement">
-            <Editor v-model="formData.studentAchievementRequirement" ref="editorRef"/>
-          </el-form-item>
-        </el-col>
-      </el-row>
-
+    <el-row  >
+      <el-col :span="24">
+        <el-form-item label="对研究生毕业时学术成果的要求" prop="studentAchievementRequirement">
+          <el-input type="textarea" v-model="formData.studentAchievementRequirement" placeholder="未填写时,默认为按学校及学院发表学术成果的要求执行" :disabled="!isSupervisor" />
+        </el-form-item>
+      </el-col>
+    </el-row>
     </el-form>
     <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm" v-if="userInfo.userType==='3'">确 定</el-button>
+      <el-button @click="dialogVisible = false" v-if="userInfo.userType==='3'" >取 消</el-button>
     </template>
   </Dialog>
 </template>
+
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { CommonStatusEnum } from '@/utils/constants'
 import * as UserApi from '@/api/system/user'
 import { FormRules } from 'element-plus'
 import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import {selectionBookVO,selectionBookApi} from '@/api/system/studentSelectSupervisorRecord/selectionBook'
+import download from '@/utils/download'
 
 defineOptions({ name: 'teacherRequireForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const userTypeSt = ref('1')
-const userTypeT = ref('3')
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
-  supervisorId:undefined,
-  nickname: '',
   id: undefined,
   sex: undefined,
-  remark: '',
+  supervisorId:undefined,
+  nickname: '',
+  title: '',//职称
   status: CommonStatusEnum.ENABLE,
-  userNumber: '',
-  major:'',
+  userNumber: '',//工号
+  major:'',//研究方向
   studentAchievementRequirement:"",
+  introduction:"",//简介
+  externalSupervisorWorkPlace:""
 })
 const formRules = reactive<FormRules>({
 })
@@ -111,50 +128,54 @@ const getUserInfo = async () => {
   const users = await getUserProfile()
   userInfo.value = users
 }
-  const open = async (supervisorId: number) => {
+// const isStudent = computed(() => userInfo.value.userType === "1");
+
+  const open = async (type:string,supervisorId: number) => {
   dialogVisible.value = true
-  dialogTitle.value = '查看详情'
-  formType.value = 'detail' // 假设你是要查看和编辑导师信息
-  resetForm()
-  // 获取导师信息
-  if (supervisorId) {
-    formLoading.value = true
-    try {
-      const user = await UserApi.getUser(supervisorId)
-      formData.value = {
-        ...user,
-        supervisorId: supervisorId, // 确保 supervisorId 也被设置
+    formType.value=type
+  if (formType.value == 'detail'){
+    dialogTitle.value = '查看详情'
+    resetForm()
+    if (supervisorId) {
+      formLoading.value = true
+      try {
+        const user = await UserApi.getUser(supervisorId)
+        formData.value = {
+          ...user,
+          supervisorId: supervisorId, // 确保 supervisorId 也被设置
+        }
+        console.log(formData.value)
+      } finally {
+        formLoading.value = false
       }
-    } finally {
-      formLoading.value = false
     }
-  }
+  } else if (formType.value == 'update'){
+      dialogTitle.value = '填写互选表信息'
+      resetForm()
+      // 获取导师信息
+        formLoading.value = true
+        try {
+          const user = await UserApi.getUser(userInfo.value.id)
+          formData.value =user
+        } finally {
+          formLoading.value = false
+        }
+      }
 }
   defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
-  // 校验表单
-  if(formType.value === 'create-S'){
-    formData.value.userType = userTypeSt.value
-  }
-  if(formType.value === 'create-T'){
-    formData.value.userType = userTypeT.value
-  }
-  console.log(formData.value,'提交的表单');
-  if (!formRef.value) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as UserApi.UserVO
-    if (formType.value === 'create-S'|| formType.value === 'create-T') {
-      await UserApi.createUser(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await UserApi.updateUser(data)
+    const data = formData.value as unknown as selectionBookVO
+    if (Array.isArray(data.introduction) && data.introduction.length > 0) {
+      data.introduction = data.introduction[0]; // 只取数组的第一个元素
+    }
+    if (formType.value === 'update') {
+      await selectionBookApi.updateSelectionBook(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -164,6 +185,16 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
+
+const isSupervisor = computed(() => userInfo.value.userType === '3');
+
+//下载PDF
+const handleDownload = () => {
+  console.log(formData.value.introduction)
+  download.markdown(formData.value.introduction, '个人简历.pdf');
+};
+
+
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
@@ -171,11 +202,13 @@ const resetForm = () => {
     nickname: '',
     id: undefined,
     sex: undefined,
-    remark: '',
+    title: '',//职称
     status: CommonStatusEnum.ENABLE,
-    userNumber: '',
-    major:'',
+    userNumber: '',//工号
+    major:'',//研究方向
     studentAchievementRequirement:"",
+    introduction:"",//简介
+    externalSupervisorWorkPlace:""
   }
   formRef.value?.resetFields()
 }
@@ -184,3 +217,37 @@ onMounted(() => {
 })
 
 </script>
+
+<style scoped>
+.custom-form {
+  background-color: #f0f4f8; /* 浅灰色背景 */
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
+}
+.custom-form .custom-download-button {
+  color: #4b7bec; /* 灰蓝色字体颜色 */
+  border: 1px solid #4b7bec; /* 灰蓝色边框 */
+  background-color: transparent; /* 透明背景 */
+  padding: 10px 20px; /* 按钮内边距 */
+  border-radius: 4px; /* 圆角边框 */
+  font-size: 16px; /* 字体大小 */
+  transition: all 0.3s ease; /* 平滑过渡效果 */
+  outline: none; /* 去除点击时的轮廓 */
+}
+.custom-form .custom-download-button:hover,
+.custom-form .custom-download-button:focus {
+  color: #fff; /* 悬浮时字体颜色 */
+  background-color: #4b7bec; /* 悬浮时背景颜色 */
+  border-color: #4b7bec; /* 悬浮时边框颜色 */
+}
+.custom-form .custom-download-button:active {
+  background-color: skyblue; /* 点击时背景颜色 */
+  border-color: #3a66b1; /* 点击时边框颜色 */
+}
+.full-width-editor {
+  width: 100%;
+  border: #e5e7ec;
+  border-radius: 4px;
+}
+</style>

+ 1 - 1
src/views/system/workroomTeacher/dept/index.vue

@@ -187,7 +187,7 @@ export default defineComponent({
 // const users = ref()
 // const getSupervisor= async () => {
 //   try {
-//     const response = await UserApi.getSupervisor()
+//     const response = await UserApi.getSupervisor()y
 //     users.value = response
 //   } catch (error) {
 //     console.error('Error fetching user data:', error)

+ 1 - 1
src/views/system/workroomTeacher/dept/student.vue

@@ -19,7 +19,7 @@
             :show-overflow-tooltip="true"
           />
           <el-table-column label="年级" align="center" prop="grade" width="120" />
-          <el-table-column label="学号" align="center" prop="studentId" width="150" />
+          <el-table-column label="学号" align="center" prop="userNumber" width="150" />
           <el-table-column label="专业" align="center" prop="major" width="120" />
           <!-- <el-table-column
             label="工作间"

+ 11 - 4
src/views/system/workroomTeacher/user/UserForm.vue

@@ -175,8 +175,15 @@
           </el-form-item>
         </el-col>
         <el-col :span="12" v-if="formType === 'create-S' || formType === 'update-S'">
-          <el-form-item label="学位类型" prop="masterType" v-if="formType === 'create-S' || formType === 'update-S'">
-            <el-input v-model="formData.masterType" placeholder="请输入学位类型" />
+          <el-form-item label="硕士类型" prop="masterType" v-if="formType === 'create-S' || formType === 'update-S'">
+            <el-select v-model="formData.masterType" placeholder="请选择硕士类型">
+              <el-option
+                v-for="option in masterTypeOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
           </el-form-item>
         </el-col>
       </el-row>
@@ -325,8 +332,8 @@ const majorOptions = [
 
 // 获取所有学位类型
 const masterTypeOptions = [
-  { value: '学硕' },
-  { value: '专硕' },
+  { value: 1, label:"学硕" },
+  { value: 2 , label:"专硕"},
 ]