Parcourir la source

feat(monthlyAssessSum): add word document export function

实现月度考核汇总页面的Word文档导出功能,支持导出页面所有图表和表格数据,自动生成带格式的报告文档,包含加载状态提示和错误处理。
huoyi il y a 1 mois
Parent
commit
897dddfaf3
1 fichiers modifiés avec 472 ajouts et 4 suppressions
  1. 472 4
      src/views/performanceManage/monthlyAssessSum/index.vue

+ 472 - 4
src/views/performanceManage/monthlyAssessSum/index.vue

@@ -178,9 +178,12 @@
178 178
 </template>
179 179
 
180 180
 <script setup>
181
-import { ref, reactive, onMounted, nextTick, computed } from 'vue'
182
-import { ElMessage } from 'element-plus'
181
+import { ref, reactive, onMounted, onUnmounted, nextTick, computed } from 'vue'
182
+import { ElMessage, ElLoading } from 'element-plus'
183 183
 import * as echarts from 'echarts'
184
+import html2canvas from 'html2canvas'
185
+import { Document, Packer, Paragraph, ImageRun, HeadingLevel, AlignmentType } from 'docx'
186
+import { saveAs } from 'file-saver'
184 187
 
185 188
 // API导入
186 189
 import {
@@ -738,10 +741,475 @@ const resetQuery = () => {
738 741
 
739 742
 // 导出
740 743
 const handleExport = async () => {
744
+  const loading = ElLoading.service({
745
+    lock: true,
746
+    text: '正在生成Word文档...',
747
+    background: 'rgba(0, 0, 0, 0.7)'
748
+  })
749
+
741 750
   try {
742
-    ElMessage.success('导出功能开发中')
751
+    const children = []
752
+
753
+    // 生成动态文档标题
754
+    let reportTitle = '非干部月度考核汇总报告'
755
+    if (queryParams.assessmentMonth) {
756
+      const [year, month] = queryParams.assessmentMonth.split('-')
757
+      reportTitle = `${year}年${parseInt(month)}月非干部月度考核汇总报告`
758
+    }
759
+
760
+    // 添加文档标题
761
+    children.push(
762
+      new Paragraph({
763
+        text: reportTitle,
764
+        heading: HeadingLevel.TITLE,
765
+        alignment: AlignmentType.CENTER,
766
+      }),
767
+      new Paragraph({
768
+        text: `导出时间: ${new Date().toLocaleString('zh-CN')}`,
769
+        alignment: AlignmentType.CENTER,
770
+      }),
771
+      new Paragraph({ text: '' })
772
+    )
773
+
774
+    // 获取所有 main-section(两大区块)
775
+    const mainSections = document.querySelectorAll('.main-section')
776
+
777
+    for (let i = 0; i < mainSections.length; i++) {
778
+      const section = mainSections[i]
779
+      const sectionTitle = section.querySelector('.section-title')
780
+      const sectionName = sectionTitle ? sectionTitle.textContent : `第${i + 1}部分`
781
+
782
+      children.push(
783
+        new Paragraph({
784
+          text: `${i + 1}. ${sectionName}`,
785
+          heading: HeadingLevel.HEADING_1,
786
+        }),
787
+        new Paragraph({ text: '' })
788
+      )
789
+
790
+      if (i === 0) {
791
+        // 第一部分:非干部月度考核分数汇总
792
+        // 处理第一行的两个图表
793
+        const chartRow1 = section.querySelector('.chart-row')
794
+        if (chartRow1) {
795
+          const chartCards = chartRow1.querySelectorAll('.chart-card')
796
+          for (let j = 0; j < chartCards.length; j++) {
797
+            const card = chartCards[j]
798
+            const header = card.querySelector('.chart-header')
799
+            const chartTitle = header ? header.textContent : `图表${j + 1}`
800
+
801
+            children.push(
802
+              new Paragraph({
803
+                text: `${i + 1}.${j + 1} ${chartTitle}`,
804
+                heading: HeadingLevel.HEADING_2,
805
+              }),
806
+              new Paragraph({ text: '' })
807
+            )
808
+
809
+            const chartContainer = card.querySelector('.chart-container')
810
+            if (chartContainer) {
811
+              await captureAndAddImage(chartContainer, children)
812
+            }
813
+          }
814
+        }
815
+
816
+        // 处理第二行的图表和汇总表
817
+        const chartRow2 = section.querySelectorAll('.chart-row')
818
+        if (chartRow2.length > 1) {
819
+          const secondRow = chartRow2[1]
820
+          const cards = secondRow.querySelectorAll('.chart-card')
821
+          for (let j = 0; j < cards.length; j++) {
822
+            const card = cards[j]
823
+            const header = card.querySelector('.chart-header')
824
+            const chartTitle = header ? header.textContent : `图表${j + 1}`
825
+
826
+            children.push(
827
+              new Paragraph({
828
+                text: `${i + 1}.${j + 3} ${chartTitle}`,
829
+                heading: HeadingLevel.HEADING_2,
830
+              }),
831
+              new Paragraph({ text: '' })
832
+            )
833
+
834
+            if (j === 0) {
835
+              // 各部门分值分布对比图
836
+              const chartContainer = card.querySelector('.chart-container')
837
+              if (chartContainer) {
838
+                await captureAndAddImage(chartContainer, children)
839
+              }
840
+            } else {
841
+              // 汇总表
842
+              const table = card.querySelector('.el-table')
843
+              if (table) {
844
+                await captureTable(table, children)
845
+              }
846
+            }
847
+          }
848
+        }
849
+      } else {
850
+        // 第二部分:非干部月度考核分类结果汇总
851
+        // 汇总统计表格
852
+        const summaryTableCard = section.querySelector('.summary-table-card')
853
+        if (summaryTableCard) {
854
+          const header = summaryTableCard.querySelector('.chart-header')
855
+          const tableTitle = header ? header.textContent : '汇总统计'
856
+
857
+          children.push(
858
+            new Paragraph({
859
+              text: `${i + 1}.1 ${tableTitle}`,
860
+              heading: HeadingLevel.HEADING_2,
861
+            }),
862
+            new Paragraph({ text: '' })
863
+          )
864
+
865
+          const table = summaryTableCard.querySelector('.el-table')
866
+          if (table) {
867
+            await captureTable(table, children)
868
+          }
869
+        }
870
+
871
+        // 第四行的两个图表
872
+        const chartRows = section.querySelectorAll('.chart-row')
873
+        if (chartRows.length > 0) {
874
+          const chartRow = chartRows[0]
875
+          const chartCards = chartRow.querySelectorAll('.chart-card')
876
+          for (let j = 0; j < chartCards.length; j++) {
877
+            const card = chartCards[j]
878
+            const header = card.querySelector('.chart-header')
879
+            const chartTitle = header ? header.textContent : `图表${j + 1}`
880
+
881
+            children.push(
882
+              new Paragraph({
883
+                text: `${i + 1}.${j + 2} ${chartTitle}`,
884
+                heading: HeadingLevel.HEADING_2,
885
+              }),
886
+              new Paragraph({ text: '' })
887
+            )
888
+
889
+            const pieChartsContainer = card.querySelector('.pie-charts-container')
890
+            if (pieChartsContainer) {
891
+              await captureAndAddImage(pieChartsContainer, children)
892
+            }
893
+          }
894
+        }
895
+      }
896
+    }
897
+
898
+    // 处理第一个遍历区域(traversalData1)
899
+    const traversalSections = document.querySelectorAll('.traversal-section')
900
+    let sectionCounter = mainSections.length + 1
901
+
902
+    if (traversalSections.length > 0) {
903
+      const firstTraversal = traversalSections[0]
904
+      const traversalContainers = firstTraversal.querySelectorAll('.traversal-container')
905
+
906
+      for (let t = 0; t < traversalContainers.length; t++) {
907
+        const container = traversalContainers[t]
908
+        const headerEl = container.querySelector('.traversal-header')
909
+        const title = headerEl ? headerEl.textContent : `部门${t + 1}`
910
+
911
+        children.push(
912
+          new Paragraph({
913
+            text: `${sectionCounter}. ${title}`,
914
+            heading: HeadingLevel.HEADING_1,
915
+          }),
916
+          new Paragraph({ text: '' })
917
+        )
918
+
919
+        const content = container.querySelector('.traversal-content')
920
+        if (content) {
921
+          // 左边表格
922
+          const tableSection = content.querySelector('.table-section')
923
+          if (tableSection) {
924
+            const table = tableSection.querySelector('.el-table')
925
+            if (table) {
926
+              children.push(
927
+                new Paragraph({
928
+                  text: `${sectionCounter}.1 考核组统计表`,
929
+                  heading: HeadingLevel.HEADING_2,
930
+                }),
931
+                new Paragraph({ text: '' })
932
+              )
933
+              await captureTable(table, children)
934
+            }
935
+          }
936
+
937
+          // 右边两个饼状图
938
+          const chartSection = content.querySelector('.chart-section')
939
+          if (chartSection) {
940
+            const pieContainers = chartSection.querySelectorAll('.pie-chart-container')
941
+            for (let p = 0; p < pieContainers.length; p++) {
942
+              const pieContainer = pieContainers[p]
943
+              const pieTitleEl = pieContainer.querySelector('.pie-chart-title')
944
+              const pieTitle = pieTitleEl ? pieTitleEl.textContent : `饼图${p + 1}`
945
+
946
+              children.push(
947
+                new Paragraph({
948
+                  text: `${sectionCounter}.${p + 2} ${pieTitle}`,
949
+                  heading: HeadingLevel.HEADING_2,
950
+                }),
951
+                new Paragraph({ text: '' })
952
+              )
953
+
954
+              const pieChart = pieContainer.querySelector('.pie-chart')
955
+              if (pieChart) {
956
+                await captureAndAddImage(pieChart, children)
957
+              }
958
+            }
959
+          }
960
+        }
961
+
962
+        sectionCounter++
963
+      }
964
+    }
965
+
966
+    // 处理第二个遍历区域(traversalData2)
967
+    if (traversalSections.length > 1) {
968
+      const secondTraversal = traversalSections[1]
969
+      const traversalContainers = secondTraversal.querySelectorAll('.traversal-container')
970
+
971
+      for (let t = 0; t < traversalContainers.length; t++) {
972
+        const container = traversalContainers[t]
973
+        const headerEl = container.querySelector('.traversal-header')
974
+        const title = headerEl ? headerEl.textContent : `部门${t + 1}`
975
+
976
+        children.push(
977
+          new Paragraph({
978
+            text: `${sectionCounter}. ${title}`,
979
+            heading: HeadingLevel.HEADING_1,
980
+          }),
981
+          new Paragraph({ text: '' })
982
+        )
983
+
984
+        const content = container.querySelector('.traversal-content')
985
+        if (content) {
986
+          // 左边表格
987
+          const tableSection = content.querySelector('.table-section')
988
+          if (tableSection) {
989
+            const table = tableSection.querySelector('.el-table')
990
+            if (table) {
991
+              children.push(
992
+                new Paragraph({
993
+                  text: `${sectionCounter}.1 考核组统计表`,
994
+                  heading: HeadingLevel.HEADING_2,
995
+                }),
996
+                new Paragraph({ text: '' })
997
+              )
998
+              await captureTable(table, children)
999
+            }
1000
+          }
1001
+
1002
+          // 右边柱状图和饼状图
1003
+          const chartSection = content.querySelector('.chart-section')
1004
+          if (chartSection) {
1005
+            const barContainer = chartSection.querySelector('.bar-chart-container')
1006
+            if (barContainer) {
1007
+              const chartTitleEl = barContainer.querySelector('.chart-title')
1008
+              const chartTitle = chartTitleEl ? chartTitleEl.textContent : '考核分数分布'
1009
+
1010
+              children.push(
1011
+                new Paragraph({
1012
+                  text: `${sectionCounter}.2 ${chartTitle}`,
1013
+                  heading: HeadingLevel.HEADING_2,
1014
+                }),
1015
+                new Paragraph({ text: '' })
1016
+              )
1017
+
1018
+              const barChart = barContainer.querySelector('.bar-chart')
1019
+              if (barChart) {
1020
+                await captureAndAddImage(barChart, children)
1021
+              }
1022
+            }
1023
+
1024
+            const pieContainer = chartSection.querySelector('.pie-chart-container')
1025
+            if (pieContainer) {
1026
+              const pieTitleEl = pieContainer.querySelector('.pie-chart-title')
1027
+              const pieTitle = pieTitleEl ? pieTitleEl.textContent : '岗位分布'
1028
+
1029
+              children.push(
1030
+                new Paragraph({
1031
+                  text: `${sectionCounter}.3 ${pieTitle}`,
1032
+                  heading: HeadingLevel.HEADING_2,
1033
+                }),
1034
+                new Paragraph({ text: '' })
1035
+              )
1036
+
1037
+              const pieChart = pieContainer.querySelector('.pie-chart')
1038
+              if (pieChart) {
1039
+                await captureAndAddImage(pieChart, children)
1040
+              }
1041
+            }
1042
+          }
1043
+        }
1044
+
1045
+        sectionCounter++
1046
+      }
1047
+    }
1048
+
1049
+    // 创建文档
1050
+    const doc = new Document({
1051
+      sections: [{
1052
+        properties: {},
1053
+        children: children
1054
+      }]
1055
+    })
1056
+
1057
+    // 生成并下载
1058
+    const blob = await Packer.toBlob(doc)
1059
+    const fileName = `${reportTitle}_${new Date().toISOString().slice(0, 10)}.docx`
1060
+    saveAs(blob, fileName)
1061
+
1062
+    ElMessage.success('Word文档导出成功')
1063
+  } catch (error) {
1064
+    console.error('导出失败:', error)
1065
+    ElMessage.error('导出失败: ' + (error.message || '请重试'))
1066
+  } finally {
1067
+    loading.close()
1068
+  }
1069
+}
1070
+
1071
+// 截图图表容器并添加为图片
1072
+const captureAndAddImage = async (element, children) => {
1073
+  try {
1074
+    const canvas = await html2canvas(element, {
1075
+      backgroundColor: '#ffffff',
1076
+      scale: 2,
1077
+      logging: false
1078
+    })
1079
+
1080
+    const imageData = canvas.toDataURL('image/png')
1081
+    const base64Data = imageData.replace(/^data:image\/png;base64,/, '')
1082
+
1083
+    if (!base64Data || base64Data.trim() === '') {
1084
+      return
1085
+    }
1086
+
1087
+    const maxWidth = 500
1088
+    const originalWidth = canvas.width
1089
+    const originalHeight = canvas.height
1090
+    const aspectRatio = originalWidth / originalHeight
1091
+
1092
+    let finalWidth = maxWidth
1093
+    let finalHeight = maxWidth / aspectRatio
1094
+
1095
+    const maxHeight = 400
1096
+    if (finalHeight > maxHeight) {
1097
+      finalHeight = maxHeight
1098
+      finalWidth = maxHeight * aspectRatio
1099
+    }
1100
+
1101
+    const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))
1102
+    children.push(
1103
+      new Paragraph({
1104
+        children: [
1105
+          new ImageRun({
1106
+            data: imageBytes,
1107
+            transformation: {
1108
+              width: Math.round(finalWidth),
1109
+              height: Math.round(finalHeight)
1110
+            }
1111
+          })
1112
+        ],
1113
+        alignment: AlignmentType.CENTER,
1114
+      }),
1115
+      new Paragraph({ text: '' })
1116
+    )
1117
+  } catch (error) {
1118
+    console.error('截图失败:', error)
1119
+  }
1120
+}
1121
+
1122
+// 截图表格并添加为图片
1123
+const captureTable = async (table, children) => {
1124
+  try {
1125
+    const originalMaxHeight = table.style.maxHeight || ''
1126
+    const originalOverflowY = table.style.overflowY || ''
1127
+    const originalOverflowX = table.style.overflowX || ''
1128
+    const originalTableWidth = table.style.width || ''
1129
+
1130
+    table.style.maxHeight = 'none'
1131
+    table.style.overflowY = 'visible'
1132
+    table.style.overflowX = 'visible'
1133
+    table.style.width = '800px'
1134
+
1135
+    let originalTableHeight = ''
1136
+    let originalTableWidthStyle = ''
1137
+    originalTableHeight = table.style.height || ''
1138
+    originalTableWidthStyle = table.style.width || ''
1139
+    table.style.height = 'auto'
1140
+    table.style.width = '800px'
1141
+
1142
+    const bodyWrapper = table.querySelector('.el-table__body-wrapper')
1143
+    if (bodyWrapper) {
1144
+      bodyWrapper.style.overflowX = 'visible'
1145
+      bodyWrapper.style.overflowY = 'visible'
1146
+    }
1147
+
1148
+    await new Promise(resolve => setTimeout(resolve, 200))
1149
+
1150
+    const canvas = await html2canvas(table, {
1151
+      backgroundColor: '#ffffff',
1152
+      scale: 1.5,
1153
+      logging: false,
1154
+      useCORS: true,
1155
+      allowTaint: true,
1156
+      scrollX: 0,
1157
+      scrollY: 0,
1158
+      width: 800,
1159
+      height: table.scrollHeight
1160
+    })
1161
+
1162
+    table.style.maxHeight = originalMaxHeight
1163
+    table.style.overflowY = originalOverflowY
1164
+    table.style.overflowX = originalOverflowX
1165
+    table.style.width = originalTableWidth
1166
+    table.style.height = originalTableHeight
1167
+    table.style.width = originalTableWidthStyle
1168
+
1169
+    if (bodyWrapper) {
1170
+      bodyWrapper.style.overflowX = ''
1171
+      bodyWrapper.style.overflowY = ''
1172
+    }
1173
+
1174
+    const imageData = canvas.toDataURL('image/png')
1175
+    const base64Data = imageData.replace(/^data:image\/png;base64,/, '')
1176
+
1177
+    if (!base64Data || base64Data.trim() === '') {
1178
+      return
1179
+    }
1180
+
1181
+    const maxWidth = 500
1182
+    const originalWidth = canvas.width
1183
+    const originalHeight = canvas.height
1184
+    const aspectRatio = originalWidth / originalHeight
1185
+
1186
+    let finalWidth = maxWidth
1187
+    let finalHeight = maxWidth / aspectRatio
1188
+
1189
+    const maxHeight = 400
1190
+    if (finalHeight > maxHeight) {
1191
+      finalHeight = maxHeight
1192
+      finalWidth = maxHeight * aspectRatio
1193
+    }
1194
+
1195
+    const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))
1196
+    children.push(
1197
+      new Paragraph({
1198
+        children: [
1199
+          new ImageRun({
1200
+            data: imageBytes,
1201
+            transformation: {
1202
+              width: Math.round(finalWidth),
1203
+              height: Math.round(finalHeight)
1204
+            }
1205
+          })
1206
+        ],
1207
+        alignment: AlignmentType.CENTER,
1208
+      }),
1209
+      new Paragraph({ text: '' })
1210
+    )
743 1211
   } catch (error) {
744
-    ElMessage.error('导出失败')
1212
+    console.error('表格截图失败:', error)
745 1213
   }
746 1214
 }
747 1215