index.vue 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. <template>
  2. <div class="app-container">
  3. <!-- 查询条件 -->
  4. <div class="filter-section">
  5. <el-card>
  6. <div class="filter-container">
  7. <el-form :model="queryParams" ref="queryFormRef" :inline="true" class="search-form">
  8. <el-form-item label="考核月份" prop="assessmentMonth">
  9. <el-date-picker v-model="queryParams.assessmentMonth" type="month" placeholder="请选择考核月份"
  10. value-format="YYYY-MM" style="width: 200px" />
  11. </el-form-item>
  12. <el-form-item>
  13. <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
  14. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  15. </el-form-item>
  16. </el-form>
  17. <div class="export-button">
  18. <el-button type="warning" icon="Document" @click="handleExport">导出文档</el-button>
  19. </div>
  20. </div>
  21. </el-card>
  22. </div>
  23. <!-- 非干部月度考核分数汇总 -->
  24. <div class="main-section">
  25. <h2 class="section-title">非干部月度考核分数汇总</h2>
  26. <!-- 第一行:两个区块 -->
  27. <div class="chart-row">
  28. <!-- 整体分值分布柱状图 -->
  29. <el-card class="chart-card">
  30. <div class="chart-header">整体分值分布柱状图</div>
  31. <div ref="overallBarChart" class="chart-container"></div>
  32. </el-card>
  33. <!-- 参与人数占比饼图 -->
  34. <el-card class="chart-card">
  35. <div class="chart-header">参与人数占比饼图</div>
  36. <div ref="participantPieChart" class="chart-container"></div>
  37. </el-card>
  38. </div>
  39. <!-- 第二行:两个区块 -->
  40. <div class="chart-row">
  41. <!-- 各部门分值分布对比图 -->
  42. <el-card class="chart-card">
  43. <div class="chart-header">各部门分值分布对比图</div>
  44. <div ref="departmentComparisonChart" class="chart-container"></div>
  45. </el-card>
  46. <!-- 汇总表 -->
  47. <el-card class="chart-card">
  48. <div class="chart-header">汇总表</div>
  49. <el-table :data="summaryTableData" border style="width: 100%; margin-top: 10px;">
  50. <el-table-column prop="rangeLabel" label="区间" align="center" min-width="100" />
  51. <el-table-column prop="totalCount" :label="summaryTableTitle" align="center" min-width="150" />
  52. <el-table-column v-for="col in summaryTableColumns" :key="col.deptId" :prop="'team' + col.deptId"
  53. :label="col.deptName" align="center" min-width="100" />
  54. </el-table>
  55. </el-card>
  56. </div>
  57. </div>
  58. <!-- 非干部月度考核分类结果汇总 -->
  59. <div class="main-section">
  60. <h2 class="section-title">非干部月度考核分类结果汇总</h2>
  61. <!-- 汇总统计表格 -->
  62. <el-card class="summary-table-card">
  63. <div class="chart-header">汇总统计</div>
  64. <el-table :data="classificationTableData" border style="width: 100%; margin-top: 10px;">
  65. <el-table-column prop="assessmentTeamCount" label="考核组人数" align="center" min-width="120" />
  66. <el-table-column prop="estimatedImprovementCount" label="测算待改进人数" align="center" min-width="140" />
  67. <el-table-column prop="improvementTotalCount" label="待改进总人数" align="center" min-width="120" />
  68. <el-table-column prop="improvementExemptedCount" label="待改进豁免" align="center" min-width="100" />
  69. <el-table-column prop="actualImprovementCount" label="实际待改进总人数" align="center" min-width="140" />
  70. <el-table-column prop="incompetentTotalCount" label="不称职总人数" align="center" min-width="120" />
  71. <el-table-column prop="incompetentExemptedCount" label="不称职豁免" align="center" min-width="100" />
  72. <el-table-column prop="actualIncompetentCount" label="实际不称职人数" align="center" min-width="140" />
  73. </el-table>
  74. </el-card>
  75. <!-- 第四行:两个区块 -->
  76. <div class="chart-row">
  77. <!-- 大队分布统计图 -->
  78. <el-card class="chart-card">
  79. <div class="chart-header">大队分布统计图</div>
  80. <div class="pie-charts-container">
  81. <div ref="brigadePieChart1" class="pie-chart"></div>
  82. <div ref="brigadePieChart2" class="pie-chart"></div>
  83. </div>
  84. </el-card>
  85. <!-- 岗位分布统计图 -->
  86. <el-card class="chart-card">
  87. <div class="chart-header">岗位分布统计图</div>
  88. <div class="pie-charts-container">
  89. <div ref="positionPieChart1" class="pie-chart"></div>
  90. <div ref="positionPieChart2" class="pie-chart"></div>
  91. </div>
  92. </el-card>
  93. </div>
  94. </div>
  95. <!-- 第一个遍历:表格和两个饼状图 -->
  96. <div class="traversal-section">
  97. <div v-for="(item, index) in traversalData1" :key="index" class="traversal-container">
  98. <div class="traversal-header">{{ item.title }}</div>
  99. <div class="traversal-content">
  100. <!-- 左边表格 -->
  101. <div class="table-section">
  102. <el-table :data="item.tableData" border style="width: 100%;">
  103. <el-table-column prop="assessmentTeam" label="考核组" align="center" min-width="100" />
  104. <el-table-column prop="assessmentTeamCount" label="考核组人数" align="center" min-width="120" />
  105. <el-table-column prop="estimatedImprovementCount" label="测算待改进人数" align="center" min-width="140" />
  106. <el-table-column prop="improvementTotalCount" label="待改进总人数" align="center" min-width="120" />
  107. <el-table-column prop="improvementExemptedCount" label="待改进豁免人数" align="center" min-width="140" />
  108. <el-table-column prop="actualImprovementCount" label="实际待改进人数" align="center" min-width="140" />
  109. <el-table-column prop="incompetentTotalCount" label="不称职总人数" align="center" min-width="120" />
  110. <el-table-column prop="incompetentExemptedCount" label="不称职豁免人数" align="center" min-width="140" />
  111. <el-table-column prop="actualIncompetentCount" label="实际不称职人数" align="center" min-width="140" />
  112. </el-table>
  113. </div>
  114. <!-- 右边两个饼状图 -->
  115. <div class="chart-section">
  116. <div class="pie-chart-container">
  117. <div class="pie-chart-title">{{ item.title }}实际待改进人数分布</div>
  118. <div :ref="el => setTraversalChartRef(el, `pieChart1_${index}`)" class="pie-chart"></div>
  119. </div>
  120. <div class="pie-chart-container">
  121. <div class="pie-chart-title">{{ item.title }}实际不称职人数分布</div>
  122. <div :ref="el => setTraversalChartRef(el, `pieChart2_${index}`)" class="pie-chart"></div>
  123. </div>
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. <!-- 第二个遍历:表格、柱状图和饼状图 -->
  129. <div class="traversal-section">
  130. <div v-for="(item, index) in traversalData2" :key="index" class="traversal-container">
  131. <div class="traversal-header">{{ item.title }}</div>
  132. <div class="traversal-content">
  133. <!-- 左边表格 -->
  134. <div class="table-section">
  135. <el-table :data="item.tableData" border style="width: 100%;">
  136. <el-table-column prop="assessmentTeam" label="考核组" align="center" min-width="100" />
  137. <el-table-column prop="assessmentTeamCount" label="考核组人数" align="center" min-width="120" />
  138. <el-table-column prop="estimatedImprovementCount" label="测算待改进人数" align="center" min-width="140" />
  139. <el-table-column prop="improvementTotalCount" label="待改进人数" align="center" min-width="120" />
  140. <el-table-column prop="improvementExemptedCount" label="豁免" align="center" min-width="80" />
  141. <el-table-column prop="actualImprovementCount" label="实际待改进人数" align="center" min-width="140" />
  142. <el-table-column prop="incompetentTotalCount" label="不称职人数" align="center" min-width="120" />
  143. <el-table-column prop="incompetentExemptedCount" label="豁免" align="center" min-width="80" />
  144. <el-table-column prop="actualIncompetentCount" label="实际不称职人数" align="center" min-width="140" />
  145. </el-table>
  146. </div>
  147. <!-- 右边柱状图和饼状图 -->
  148. <div class="chart-section">
  149. <div class="bar-chart-container">
  150. <div class="chart-title">{{ item.title }}考核人数与待改进情况</div>
  151. <div :ref="el => setTraversalChartRef(el, `barChart_${index}`)" class="bar-chart"></div>
  152. </div>
  153. <div class="pie-chart-container">
  154. <div class="pie-chart-title">{{ item.title }}考核人数占比</div>
  155. <div :ref="el => setTraversalChartRef(el, `pieChart3_${index}`)" class="pie-chart"></div>
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </template>
  163. <script setup>
  164. import { ref, reactive, onMounted, onUnmounted, nextTick, computed } from 'vue'
  165. import { ElMessage, ElLoading } from 'element-plus'
  166. import * as echarts from 'echarts'
  167. import html2canvas from 'html2canvas'
  168. import { Document, Packer, Paragraph, ImageRun, HeadingLevel, AlignmentType } from 'docx'
  169. import { saveAs } from 'file-saver'
  170. // API导入
  171. import {
  172. getScoreDistribution,
  173. getDeptParticipation,
  174. getDeptScoreDistribution,
  175. getAssessmentSummary,
  176. getActualImprovementDistribution,
  177. getActualIncompetentDistribution,
  178. getAssessmentTeamImprovementDistribution,
  179. getAssessmentTeamIncompetentDistribution,
  180. getDeptAssessmentTeamStatistics,
  181. getBrigadeImprovementDistribution,
  182. getBrigadeIncompetentDistribution,
  183. getFunctionalDeptSummary,
  184. getFunctionalDeptPersonnelDistribution,
  185. getFunctionalDeptDistributionPie
  186. } from '@/api/performance/monthlyAssessSum.js'
  187. import { listDept } from '@/api/system/dept.js'
  188. // 响应式数据
  189. const loading = ref(false)
  190. const queryFormRef = ref()
  191. // 查询参数
  192. const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`
  193. const queryParams = reactive({
  194. assessmentMonth: currentMonth
  195. })
  196. const summaryTableTitle = computed(() => {
  197. if (queryParams.assessmentMonth) {
  198. const [year, month] = queryParams.assessmentMonth.split('-')
  199. return `${year}年${parseInt(month)}月模拟分数汇总`
  200. }
  201. return '模拟分数汇总'
  202. })
  203. // 图表引用
  204. const overallBarChart = ref(null)
  205. const participantPieChart = ref(null)
  206. const departmentComparisonChart = ref(null)
  207. const brigadePieChart1 = ref(null)
  208. const brigadePieChart2 = ref(null)
  209. const positionPieChart1 = ref(null)
  210. const positionPieChart2 = ref(null)
  211. // 遍历图表引用
  212. const traversalChartsRefs = ref({})
  213. // 表格数据
  214. const summaryTableData = ref([])
  215. const summaryTableColumns = ref([])
  216. const classificationTableData = ref([
  217. ])
  218. // 遍历数据
  219. const traversalData1 = ref([
  220. ])
  221. const traversalData2 = ref([
  222. ])
  223. // 图表实例
  224. let overallBarChartInstance = null
  225. let participantPieChartInstance = null
  226. let departmentComparisonChartInstance = null
  227. let brigadePieChart1Instance = null
  228. let brigadePieChart2Instance = null
  229. let positionPieChart1Instance = null
  230. let positionPieChart2Instance = null
  231. // 初始化图表
  232. const initCharts = () => {
  233. nextTick(() => {
  234. // 整体分值分布柱状图
  235. if (overallBarChart.value) {
  236. overallBarChartInstance = echarts.init(overallBarChart.value)
  237. overallBarChartInstance.setOption({
  238. tooltip: { trigger: 'axis' },
  239. xAxis: { type: 'category', data: ['90-100分', '80-89分', '70-79分', '60-69分', '60分以下'] },
  240. yAxis: { type: 'value' },
  241. series: [{ type: 'bar', data: [85, 120, 95, 60, 20], itemStyle: { color: '#3b82f6' } }]
  242. })
  243. }
  244. // 参与人数占比饼图
  245. if (participantPieChart.value) {
  246. participantPieChartInstance = echarts.init(participantPieChart.value)
  247. participantPieChartInstance.setOption({
  248. tooltip: { trigger: 'item' },
  249. series: [{
  250. type: 'pie',
  251. radius: '70%',
  252. data: [
  253. { value: 380, name: '参与考核' },
  254. { value: 20, name: '未参与考核' }
  255. ]
  256. }]
  257. })
  258. }
  259. // 各部门分值分布对比图
  260. if (departmentComparisonChart.value) {
  261. departmentComparisonChartInstance = echarts.init(departmentComparisonChart.value)
  262. departmentComparisonChartInstance.setOption({
  263. tooltip: { trigger: 'axis' },
  264. legend: { data: ['一队', '二队', '三队'] },
  265. xAxis: { type: 'category', data: ['90-100分', '80-89分', '70-79分', '60-69分', '60分以下'] },
  266. yAxis: { type: 'value' },
  267. series: [
  268. { name: '一队', type: 'bar', data: [15, 30, 25, 15, 5] },
  269. { name: '二队', type: 'bar', data: [20, 35, 30, 20, 8] },
  270. { name: '三队', type: 'bar', data: [25, 40, 35, 25, 7] }
  271. ]
  272. })
  273. }
  274. // 大队分布饼图1
  275. if (brigadePieChart1.value) {
  276. brigadePieChart1Instance = echarts.init(brigadePieChart1.value)
  277. brigadePieChart1Instance.setOption({
  278. tooltip: { trigger: 'item' },
  279. series: [{
  280. type: 'pie',
  281. radius: '50%',
  282. data: [
  283. { value: 120, name: '一队' },
  284. { value: 150, name: '二队' },
  285. { value: 130, name: '三队' }
  286. ]
  287. }]
  288. })
  289. }
  290. // 大队分布饼图2
  291. if (brigadePieChart2.value) {
  292. brigadePieChart2Instance = echarts.init(brigadePieChart2.value)
  293. brigadePieChart2Instance.setOption({
  294. tooltip: { trigger: 'item' },
  295. series: [{
  296. type: 'pie',
  297. radius: '50%',
  298. data: [
  299. { value: 45, name: '优秀' },
  300. { value: 280, name: '合格' },
  301. { value: 75, name: '待改进' }
  302. ]
  303. }]
  304. })
  305. }
  306. // 岗位分布饼图1
  307. if (positionPieChart1.value) {
  308. positionPieChart1Instance = echarts.init(positionPieChart1.value)
  309. positionPieChart1Instance.setOption({
  310. tooltip: { trigger: 'item' },
  311. series: [{
  312. type: 'pie',
  313. radius: '50%',
  314. data: [
  315. { value: 150, name: '安检员' },
  316. { value: 120, name: '设备操作员' },
  317. { value: 80, name: '管理人员' },
  318. { value: 50, name: '其他' }
  319. ]
  320. }]
  321. })
  322. }
  323. // 岗位分布饼图2
  324. if (positionPieChart2.value) {
  325. positionPieChart2Instance = echarts.init(positionPieChart2.value)
  326. positionPieChart2Instance.setOption({
  327. tooltip: { trigger: 'item' },
  328. series: [{
  329. type: 'pie',
  330. radius: '50%',
  331. data: [
  332. { value: 35, name: '优秀' },
  333. { value: 320, name: '合格' },
  334. { value: 45, name: '待改进' }
  335. ]
  336. }]
  337. })
  338. }
  339. })
  340. }
  341. // 设置遍历图表引用
  342. const setTraversalChartRef = (el, key) => {
  343. if (el) {
  344. traversalChartsRefs.value[key] = el
  345. }
  346. }
  347. // 初始化遍历图表
  348. const initTraversalCharts = () => {
  349. nextTick(() => {
  350. // 第一个遍历的饼状图
  351. traversalData1.value.forEach((item, index) => {
  352. // 饼状图1:考核结果分布
  353. const pieChart1Key = `pieChart1_${index}`
  354. if (traversalChartsRefs.value[pieChart1Key]) {
  355. const pieChart1 = echarts.init(traversalChartsRefs.value[pieChart1Key])
  356. pieChart1.setOption({
  357. tooltip: { trigger: 'item' },
  358. series: [{
  359. type: 'pie',
  360. radius: '70%',
  361. data: [
  362. { value: 120, name: '优秀' },
  363. { value: 280, name: '合格' },
  364. { value: 75, name: '待改进' }
  365. ]
  366. }]
  367. })
  368. }
  369. // 饼状图2:改进情况分布
  370. const pieChart2Key = `pieChart2_${index}`
  371. if (traversalChartsRefs.value[pieChart2Key]) {
  372. const pieChart2 = echarts.init(traversalChartsRefs.value[pieChart2Key])
  373. pieChart2.setOption({
  374. tooltip: { trigger: 'item' },
  375. series: [{
  376. type: 'pie',
  377. radius: '70%',
  378. data: [
  379. { value: 15, name: '已改进' },
  380. { value: 60, name: '待改进' },
  381. { value: 5, name: '未改进' }
  382. ]
  383. }]
  384. })
  385. }
  386. })
  387. // 第二个遍历的图表
  388. traversalData2.value.forEach((item, index) => {
  389. // 柱状图:考核分数分布
  390. const barChartKey = `barChart_${index}`
  391. if (traversalChartsRefs.value[barChartKey]) {
  392. const barChart = echarts.init(traversalChartsRefs.value[barChartKey])
  393. barChart.setOption({
  394. tooltip: { trigger: 'axis' },
  395. xAxis: { type: 'category', data: ['90-100分', '80-89分', '70-79分', '60-69分', '60分以下'] },
  396. yAxis: { type: 'value' },
  397. series: [{ type: 'bar', data: [85, 120, 95, 60, 20], itemStyle: { color: '#3b82f6' } }]
  398. })
  399. }
  400. // 饼状图:岗位分布
  401. const pieChart3Key = `pieChart3_${index}`
  402. if (traversalChartsRefs.value[pieChart3Key]) {
  403. const pieChart3 = echarts.init(traversalChartsRefs.value[pieChart3Key])
  404. pieChart3.setOption({
  405. tooltip: { trigger: 'item' },
  406. series: [{
  407. type: 'pie',
  408. radius: '70%',
  409. data: [
  410. { value: 150, name: '安检员' },
  411. { value: 120, name: '设备操作员' },
  412. { value: 80, name: '管理人员' },
  413. { value: 50, name: '其他' }
  414. ]
  415. }]
  416. })
  417. }
  418. })
  419. })
  420. }
  421. // 窗口大小变化时重绘图表
  422. const handleResize = () => {
  423. const charts = [
  424. overallBarChartInstance,
  425. participantPieChartInstance,
  426. departmentComparisonChartInstance,
  427. brigadePieChart1Instance,
  428. brigadePieChart2Instance,
  429. positionPieChart1Instance,
  430. positionPieChart2Instance
  431. ]
  432. charts.forEach(chart => {
  433. if (chart) chart.resize()
  434. })
  435. }
  436. // 获取数据
  437. const getList = async () => {
  438. loading.value = true
  439. try {
  440. // 格式化参数:YYYY-MM -> YYYYMM
  441. const formatParams = (params) => {
  442. const formatted = { ...params }
  443. if (formatted.assessmentMonth) {
  444. formatted.assessmentMonth = formatted.assessmentMonth.replace('-', '')
  445. }
  446. return formatted
  447. }
  448. // 获取大队列表
  449. const brigadeListRes = await listDept()
  450. const brigadeList = (brigadeListRes.data || []).filter(item => item.deptType === 'BRIGADE' && item.isFunctionalDept == 0)
  451. // 其他接口调用
  452. const formattedQueryParams = formatParams(queryParams)
  453. const results = await Promise.allSettled([
  454. getScoreDistribution(formattedQueryParams),
  455. getDeptParticipation(formattedQueryParams),
  456. getDeptScoreDistribution(formattedQueryParams),
  457. getAssessmentSummary(formattedQueryParams),
  458. getActualImprovementDistribution(formattedQueryParams),
  459. getActualIncompetentDistribution(formattedQueryParams),
  460. getAssessmentTeamImprovementDistribution(formattedQueryParams),
  461. getAssessmentTeamIncompetentDistribution(formattedQueryParams),
  462. getFunctionalDeptSummary(formattedQueryParams),
  463. getFunctionalDeptPersonnelDistribution(formattedQueryParams),
  464. getFunctionalDeptDistributionPie(formattedQueryParams)
  465. ])
  466. const [scoreDistRes, deptPartRes, deptScoreRes, assessmentSumRes, actualImproveRes, actualIncompRes, teamImproveRes, teamIncompRes, functionalDeptRes, functionalDeptPersonnelRes, functionalDeptPieRes] = results
  467. // 对每个大队调用三个接口
  468. const brigadeDataPromises = brigadeList.map(async (brigade) => {
  469. const brigadeParams = formatParams({ ...queryParams, deptId: brigade.deptId })
  470. const brigadeResults = await Promise.allSettled([
  471. getDeptAssessmentTeamStatistics(brigadeParams),
  472. getBrigadeImprovementDistribution(brigadeParams),
  473. getBrigadeIncompetentDistribution(brigadeParams)
  474. ])
  475. const [deptTeamStatRes, brigadeImproveRes, brigadeIncompRes] = brigadeResults
  476. const statisticsData = deptTeamStatRes.status === 'fulfilled' ? (deptTeamStatRes.value.data || []) : []
  477. const brigadeImproveData = brigadeImproveRes.status === 'fulfilled' ? (brigadeImproveRes.value.data || []) : []
  478. const brigadeIncompData = brigadeIncompRes.status === 'fulfilled' ? (brigadeIncompRes.value.data || []) : []
  479. // 解析表格数据
  480. let tableData = []
  481. if (statisticsData.length > 0 && statisticsData[0].assessmentTeams) {
  482. tableData = statisticsData[0].assessmentTeams
  483. }
  484. // 计算总计用于标题
  485. const totalImprovement = tableData.reduce((sum, item) => sum + (item.actualImprovementCount || 0), 0)
  486. const totalIncompetent = tableData.reduce((sum, item) => sum + (item.actualIncompetentCount || 0), 0)
  487. return {
  488. title: brigade.deptName,
  489. deptId: brigade.deptId,
  490. tableData,
  491. improveData: brigadeImproveData,
  492. incompData: brigadeIncompData,
  493. totalImprovement,
  494. totalIncompetent
  495. }
  496. })
  497. traversalData1.value = await Promise.all(brigadeDataPromises)
  498. traversalData1.value = traversalData1.value.filter(item => item.tableData?.length > 0 || item.improveData?.length > 0 || item.incompData?.length > 0)
  499. console.log(traversalData1.value)
  500. // debugger
  501. if (scoreDistRes.status === 'fulfilled' && scoreDistRes.value.data) {
  502. const data = scoreDistRes.value.data
  503. if (overallBarChartInstance && Array.isArray(data)) {
  504. overallBarChartInstance.setOption({
  505. xAxis: { data: data.map(item => item.scoreRange) },
  506. series: [{ data: data.map(item => item.count) }]
  507. })
  508. }
  509. }
  510. if (deptPartRes.status === 'fulfilled' && deptPartRes.value.data) {
  511. const data = deptPartRes.value.data
  512. if (participantPieChartInstance && Array.isArray(data)) {
  513. participantPieChartInstance.setOption({
  514. series: [{ data: data.map(item => ({ name: item.deptName, value: item.count })) }]
  515. })
  516. }
  517. }
  518. if (deptScoreRes.status === 'fulfilled' && deptScoreRes.value.data) {
  519. const data = deptScoreRes.value.data
  520. if (departmentComparisonChartInstance && Array.isArray(data)) {
  521. const xAxisData = data.map(item => item.rangeLabel)
  522. const firstDeptCounts = data.length > 0 ? data[0].deptCounts : []
  523. const series = firstDeptCounts.map(dept => ({
  524. name: dept.deptName,
  525. type: 'bar',
  526. data: data.map(item => {
  527. const deptData = item.deptCounts.find(d => d.deptId === dept.deptId)
  528. return deptData ? deptData.count : 0
  529. })
  530. }))
  531. departmentComparisonChartInstance.setOption({
  532. legend: { data: firstDeptCounts.map(d => d.deptName) },
  533. xAxis: { data: xAxisData },
  534. series: series
  535. })
  536. summaryTableColumns.value = firstDeptCounts
  537. summaryTableData.value = data.map(item => {
  538. const row = {
  539. rangeLabel: item.rangeLabel,
  540. totalCount: item.totalCount
  541. }
  542. item.deptCounts.forEach(dept => {
  543. row['team' + dept.deptId] = dept.count
  544. })
  545. return row
  546. })
  547. }
  548. }
  549. if (assessmentSumRes.status === 'fulfilled' && assessmentSumRes.value.data) {
  550. const data = assessmentSumRes.value.data
  551. if (data && typeof data === 'object') {
  552. classificationTableData.value = [data]
  553. }
  554. }
  555. if (actualImproveRes.status === 'fulfilled' && actualImproveRes.value.data) {
  556. const data = actualImproveRes.value.data
  557. if (brigadePieChart1Instance && Array.isArray(data)) {
  558. brigadePieChart1Instance.setOption({
  559. series: [{ data: data.map(item => ({ name: item.deptName, value: item.count })) }]
  560. })
  561. }
  562. }
  563. if (actualIncompRes.status === 'fulfilled' && actualIncompRes.value.data) {
  564. const data = actualIncompRes.value.data
  565. if (brigadePieChart2Instance && Array.isArray(data)) {
  566. brigadePieChart2Instance.setOption({
  567. series: [{ data: data.map(item => ({ name: item.deptName, value: item.count })) }]
  568. })
  569. }
  570. }
  571. if (teamImproveRes.status === 'fulfilled' && teamImproveRes.value.data) {
  572. const data = teamImproveRes.value.data
  573. if (positionPieChart1Instance && Array.isArray(data)) {
  574. positionPieChart1Instance.setOption({
  575. series: [{ data: data.map(item => ({ name: item.assessmentTeam, value: item.count })) }]
  576. })
  577. }
  578. }
  579. if (teamIncompRes.status === 'fulfilled' && teamIncompRes.value.data) {
  580. const data = teamIncompRes.value.data
  581. if (positionPieChart2Instance && Array.isArray(data)) {
  582. positionPieChart2Instance.setOption({
  583. series: [{ data: data.map(item => ({ name: item.assessmentTeam, value: item.count })) }]
  584. })
  585. }
  586. }
  587. // 初始化traversalData1对应的图表
  588. nextTick(() => {
  589. traversalData1.value.forEach((item, index) => {
  590. const chartKey1 = `pieChart1_${index}`
  591. const chartKey2 = `pieChart2_${index}`
  592. if (traversalChartsRefs.value[chartKey1] && item.improveData) {
  593. const pieChart1 = echarts.init(traversalChartsRefs.value[chartKey1])
  594. pieChart1.setOption({
  595. tooltip: { trigger: 'item' },
  596. series: [{
  597. type: 'pie',
  598. radius: '70%',
  599. data: item.improveData.map(d => ({ name: d.assessmentTeam, value: d.count }))
  600. }]
  601. })
  602. }
  603. if (traversalChartsRefs.value[chartKey2] && item.incompData) {
  604. const pieChart2 = echarts.init(traversalChartsRefs.value[chartKey2])
  605. pieChart2.setOption({
  606. tooltip: { trigger: 'item' },
  607. series: [{
  608. type: 'pie',
  609. radius: '70%',
  610. data: item.incompData.map(d => ({ name: d.assessmentTeam, value: d.count }))
  611. }]
  612. })
  613. }
  614. })
  615. })
  616. if (functionalDeptRes.status === 'fulfilled' && functionalDeptRes.value.data) {
  617. const summaryData = functionalDeptRes.value.data || []
  618. const personnelData = functionalDeptPersonnelRes.status === 'fulfilled' ? (functionalDeptPersonnelRes.value.data || []) : []
  619. const pieData = functionalDeptPieRes.status === 'fulfilled' ? (functionalDeptPieRes.value.data || []) : []
  620. traversalData2.value = summaryData.map(item => {
  621. // 匹配人员数据
  622. const personnelItem = personnelData.find(p => p.deptId === item.deptId)
  623. // 匹配饼图数据
  624. const deptPieData = pieData.filter(p => p.deptId === item.deptId)
  625. return {
  626. title: item.deptName,
  627. deptId: item.deptId,
  628. tableData: item.assessmentTeams || [],
  629. barData: personnelItem ? [
  630. { name: '考核组人数', value: personnelItem.assessmentTeamCount || 0 },
  631. { name: '实际待改进人数', value: personnelItem.actualImprovementCount || 0 },
  632. { name: '实际不称职人数', value: personnelItem.actualIncompetentCount || 0 }
  633. ] : [],
  634. pieData: deptPieData.map(p => ({ name: p.assessmentTeam, value: p.count }))
  635. }
  636. })
  637. nextTick(() => {
  638. traversalData2.value.forEach((item, index) => {
  639. const chartKey = `barChart_${index}`
  640. const pieChartKey = `pieChart3_${index}`
  641. if (traversalChartsRefs.value[chartKey] && item.barData.length > 0) {
  642. const barChart = echarts.init(traversalChartsRefs.value[chartKey])
  643. barChart.setOption({
  644. tooltip: { trigger: 'axis' },
  645. xAxis: { type: 'category', data: item.barData.map(d => d.name) },
  646. yAxis: { type: 'value' },
  647. series: [{ type: 'bar', data: item.barData.map(d => d.value), itemStyle: { color: '#3b82f6' } }]
  648. })
  649. }
  650. if (traversalChartsRefs.value[pieChartKey] && item.pieData.length > 0) {
  651. const pieChart = echarts.init(traversalChartsRefs.value[pieChartKey])
  652. pieChart.setOption({
  653. tooltip: { trigger: 'item' },
  654. series: [{ type: 'pie', radius: '70%', data: item.pieData }]
  655. })
  656. }
  657. })
  658. })
  659. }
  660. } catch (error) {
  661. console.error('获取汇总数据失败:', error)
  662. ElMessage.error('获取汇总数据失败')
  663. } finally {
  664. loading.value = false
  665. }
  666. }
  667. // 查询
  668. const handleQuery = () => {
  669. getList()
  670. }
  671. // 重置查询
  672. const resetQuery = () => {
  673. queryFormRef.value?.resetFields()
  674. getList()
  675. }
  676. // 导出
  677. const handleExport = async () => {
  678. const loading = ElLoading.service({
  679. lock: true,
  680. text: '正在生成Word文档...',
  681. background: 'rgba(0, 0, 0, 0.7)'
  682. })
  683. try {
  684. const children = []
  685. // 生成动态文档标题
  686. let reportTitle = '非干部月度考核汇总报告'
  687. if (queryParams.assessmentMonth) {
  688. const [year, month] = queryParams.assessmentMonth.split('-')
  689. reportTitle = `${year}年${parseInt(month)}月非干部月度考核汇总报告`
  690. }
  691. // 添加文档标题
  692. children.push(
  693. new Paragraph({
  694. text: reportTitle,
  695. heading: HeadingLevel.TITLE,
  696. alignment: AlignmentType.CENTER,
  697. }),
  698. new Paragraph({
  699. text: `导出时间: ${new Date().toLocaleString('zh-CN')}`,
  700. alignment: AlignmentType.CENTER,
  701. }),
  702. new Paragraph({ text: '' })
  703. )
  704. // 获取所有 main-section(两大区块)
  705. const mainSections = document.querySelectorAll('.main-section')
  706. for (let i = 0; i < mainSections.length; i++) {
  707. const section = mainSections[i]
  708. const sectionTitle = section.querySelector('.section-title')
  709. const sectionName = sectionTitle ? sectionTitle.textContent : `第${i + 1}部分`
  710. children.push(
  711. new Paragraph({
  712. text: `${i + 1}. ${sectionName}`,
  713. heading: HeadingLevel.HEADING_1,
  714. }),
  715. new Paragraph({ text: '' })
  716. )
  717. if (i === 0) {
  718. // 第一部分:非干部月度考核分数汇总
  719. // 处理第一行的两个图表
  720. const chartRow1 = section.querySelector('.chart-row')
  721. if (chartRow1) {
  722. const chartCards = chartRow1.querySelectorAll('.chart-card')
  723. for (let j = 0; j < chartCards.length; j++) {
  724. const card = chartCards[j]
  725. const header = card.querySelector('.chart-header')
  726. const chartTitle = header ? header.textContent : `图表${j + 1}`
  727. children.push(
  728. new Paragraph({
  729. text: `${i + 1}.${j + 1} ${chartTitle}`,
  730. heading: HeadingLevel.HEADING_2,
  731. }),
  732. new Paragraph({ text: '' })
  733. )
  734. const chartContainer = card.querySelector('.chart-container')
  735. if (chartContainer) {
  736. await captureAndAddImage(chartContainer, children)
  737. }
  738. }
  739. }
  740. // 处理第二行的图表和汇总表
  741. const chartRow2 = section.querySelectorAll('.chart-row')
  742. if (chartRow2.length > 1) {
  743. const secondRow = chartRow2[1]
  744. const cards = secondRow.querySelectorAll('.chart-card')
  745. for (let j = 0; j < cards.length; j++) {
  746. const card = cards[j]
  747. const header = card.querySelector('.chart-header')
  748. const chartTitle = header ? header.textContent : `图表${j + 1}`
  749. children.push(
  750. new Paragraph({
  751. text: `${i + 1}.${j + 3} ${chartTitle}`,
  752. heading: HeadingLevel.HEADING_2,
  753. }),
  754. new Paragraph({ text: '' })
  755. )
  756. if (j === 0) {
  757. // 各部门分值分布对比图
  758. const chartContainer = card.querySelector('.chart-container')
  759. if (chartContainer) {
  760. await captureAndAddImage(chartContainer, children)
  761. }
  762. } else {
  763. // 汇总表
  764. const table = card.querySelector('.el-table')
  765. if (table) {
  766. await captureTable(table, children)
  767. }
  768. }
  769. }
  770. }
  771. } else {
  772. // 第二部分:非干部月度考核分类结果汇总
  773. // 汇总统计表格
  774. const summaryTableCard = section.querySelector('.summary-table-card')
  775. if (summaryTableCard) {
  776. const header = summaryTableCard.querySelector('.chart-header')
  777. const tableTitle = header ? header.textContent : '汇总统计'
  778. children.push(
  779. new Paragraph({
  780. text: `${i + 1}.1 ${tableTitle}`,
  781. heading: HeadingLevel.HEADING_2,
  782. }),
  783. new Paragraph({ text: '' })
  784. )
  785. const table = summaryTableCard.querySelector('.el-table')
  786. if (table) {
  787. await captureTable(table, children)
  788. }
  789. }
  790. // 第四行的两个图表
  791. const chartRows = section.querySelectorAll('.chart-row')
  792. if (chartRows.length > 0) {
  793. const chartRow = chartRows[0]
  794. const chartCards = chartRow.querySelectorAll('.chart-card')
  795. for (let j = 0; j < chartCards.length; j++) {
  796. const card = chartCards[j]
  797. const header = card.querySelector('.chart-header')
  798. const chartTitle = header ? header.textContent : `图表${j + 1}`
  799. children.push(
  800. new Paragraph({
  801. text: `${i + 1}.${j + 2} ${chartTitle}`,
  802. heading: HeadingLevel.HEADING_2,
  803. }),
  804. new Paragraph({ text: '' })
  805. )
  806. const pieChartsContainer = card.querySelector('.pie-charts-container')
  807. if (pieChartsContainer) {
  808. await captureAndAddImage(pieChartsContainer, children)
  809. }
  810. }
  811. }
  812. }
  813. }
  814. // 处理第一个遍历区域(traversalData1)
  815. const traversalSections = document.querySelectorAll('.traversal-section')
  816. let sectionCounter = mainSections.length + 1
  817. if (traversalSections.length > 0) {
  818. const firstTraversal = traversalSections[0]
  819. const traversalContainers = firstTraversal.querySelectorAll('.traversal-container')
  820. for (let t = 0; t < traversalContainers.length; t++) {
  821. const container = traversalContainers[t]
  822. const headerEl = container.querySelector('.traversal-header')
  823. const title = headerEl ? headerEl.textContent : `部门${t + 1}`
  824. children.push(
  825. new Paragraph({
  826. text: `${sectionCounter}. ${title}`,
  827. heading: HeadingLevel.HEADING_1,
  828. }),
  829. new Paragraph({ text: '' })
  830. )
  831. const content = container.querySelector('.traversal-content')
  832. if (content) {
  833. // 左边表格
  834. const tableSection = content.querySelector('.table-section')
  835. if (tableSection) {
  836. const table = tableSection.querySelector('.el-table')
  837. if (table) {
  838. children.push(
  839. new Paragraph({
  840. text: `${sectionCounter}.1 考核组统计表`,
  841. heading: HeadingLevel.HEADING_2,
  842. }),
  843. new Paragraph({ text: '' })
  844. )
  845. await captureTable(table, children)
  846. }
  847. }
  848. // 右边两个饼状图
  849. const chartSection = content.querySelector('.chart-section')
  850. if (chartSection) {
  851. const pieContainers = chartSection.querySelectorAll('.pie-chart-container')
  852. for (let p = 0; p < pieContainers.length; p++) {
  853. const pieContainer = pieContainers[p]
  854. const pieTitleEl = pieContainer.querySelector('.pie-chart-title')
  855. const pieTitle = pieTitleEl ? pieTitleEl.textContent : `饼图${p + 1}`
  856. children.push(
  857. new Paragraph({
  858. text: `${sectionCounter}.${p + 2} ${pieTitle}`,
  859. heading: HeadingLevel.HEADING_2,
  860. }),
  861. new Paragraph({ text: '' })
  862. )
  863. const pieChart = pieContainer.querySelector('.pie-chart')
  864. if (pieChart) {
  865. await captureAndAddImage(pieChart, children)
  866. }
  867. }
  868. }
  869. }
  870. sectionCounter++
  871. }
  872. }
  873. // 处理第二个遍历区域(traversalData2)
  874. if (traversalSections.length > 1) {
  875. const secondTraversal = traversalSections[1]
  876. const traversalContainers = secondTraversal.querySelectorAll('.traversal-container')
  877. for (let t = 0; t < traversalContainers.length; t++) {
  878. const container = traversalContainers[t]
  879. const headerEl = container.querySelector('.traversal-header')
  880. const title = headerEl ? headerEl.textContent : `部门${t + 1}`
  881. children.push(
  882. new Paragraph({
  883. text: `${sectionCounter}. ${title}`,
  884. heading: HeadingLevel.HEADING_1,
  885. }),
  886. new Paragraph({ text: '' })
  887. )
  888. const content = container.querySelector('.traversal-content')
  889. if (content) {
  890. // 左边表格
  891. const tableSection = content.querySelector('.table-section')
  892. if (tableSection) {
  893. const table = tableSection.querySelector('.el-table')
  894. if (table) {
  895. children.push(
  896. new Paragraph({
  897. text: `${sectionCounter}.1 考核组统计表`,
  898. heading: HeadingLevel.HEADING_2,
  899. }),
  900. new Paragraph({ text: '' })
  901. )
  902. await captureTable(table, children)
  903. }
  904. }
  905. // 右边柱状图和饼状图
  906. const chartSection = content.querySelector('.chart-section')
  907. if (chartSection) {
  908. const barContainer = chartSection.querySelector('.bar-chart-container')
  909. if (barContainer) {
  910. const chartTitleEl = barContainer.querySelector('.chart-title')
  911. const chartTitle = chartTitleEl ? chartTitleEl.textContent : '考核分数分布'
  912. children.push(
  913. new Paragraph({
  914. text: `${sectionCounter}.2 ${chartTitle}`,
  915. heading: HeadingLevel.HEADING_2,
  916. }),
  917. new Paragraph({ text: '' })
  918. )
  919. const barChart = barContainer.querySelector('.bar-chart')
  920. if (barChart) {
  921. await captureAndAddImage(barChart, children)
  922. }
  923. }
  924. const pieContainer = chartSection.querySelector('.pie-chart-container')
  925. if (pieContainer) {
  926. const pieTitleEl = pieContainer.querySelector('.pie-chart-title')
  927. const pieTitle = pieTitleEl ? pieTitleEl.textContent : '岗位分布'
  928. children.push(
  929. new Paragraph({
  930. text: `${sectionCounter}.3 ${pieTitle}`,
  931. heading: HeadingLevel.HEADING_2,
  932. }),
  933. new Paragraph({ text: '' })
  934. )
  935. const pieChart = pieContainer.querySelector('.pie-chart')
  936. if (pieChart) {
  937. await captureAndAddImage(pieChart, children)
  938. }
  939. }
  940. }
  941. }
  942. sectionCounter++
  943. }
  944. }
  945. // 创建文档
  946. const doc = new Document({
  947. sections: [{
  948. properties: {},
  949. children: children
  950. }]
  951. })
  952. // 生成并下载
  953. const blob = await Packer.toBlob(doc)
  954. const fileName = `${reportTitle}_${new Date().toISOString().slice(0, 10)}.docx`
  955. saveAs(blob, fileName)
  956. ElMessage.success('Word文档导出成功')
  957. } catch (error) {
  958. console.error('导出失败:', error)
  959. ElMessage.error('导出失败: ' + (error.message || '请重试'))
  960. } finally {
  961. loading.close()
  962. }
  963. }
  964. // 截图图表容器并添加为图片
  965. const captureAndAddImage = async (element, children) => {
  966. try {
  967. const canvas = await html2canvas(element, {
  968. backgroundColor: '#ffffff',
  969. scale: 2,
  970. logging: false
  971. })
  972. const imageData = canvas.toDataURL('image/png')
  973. const base64Data = imageData.replace(/^data:image\/png;base64,/, '')
  974. if (!base64Data || base64Data.trim() === '') {
  975. return
  976. }
  977. const maxWidth = 500
  978. const originalWidth = canvas.width
  979. const originalHeight = canvas.height
  980. const aspectRatio = originalWidth / originalHeight
  981. let finalWidth = maxWidth
  982. let finalHeight = maxWidth / aspectRatio
  983. const maxHeight = 400
  984. if (finalHeight > maxHeight) {
  985. finalHeight = maxHeight
  986. finalWidth = maxHeight * aspectRatio
  987. }
  988. const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))
  989. children.push(
  990. new Paragraph({
  991. children: [
  992. new ImageRun({
  993. data: imageBytes,
  994. transformation: {
  995. width: Math.round(finalWidth),
  996. height: Math.round(finalHeight)
  997. }
  998. })
  999. ],
  1000. alignment: AlignmentType.CENTER,
  1001. }),
  1002. new Paragraph({ text: '' })
  1003. )
  1004. } catch (error) {
  1005. console.error('截图失败:', error)
  1006. }
  1007. }
  1008. // 截图表格并添加为图片
  1009. const captureTable = async (table, children) => {
  1010. try {
  1011. const originalMaxHeight = table.style.maxHeight || ''
  1012. const originalOverflowY = table.style.overflowY || ''
  1013. const originalOverflowX = table.style.overflowX || ''
  1014. const originalTableWidth = table.style.width || ''
  1015. table.style.maxHeight = 'none'
  1016. table.style.overflowY = 'visible'
  1017. table.style.overflowX = 'visible'
  1018. table.style.width = '800px'
  1019. let originalTableHeight = ''
  1020. let originalTableWidthStyle = ''
  1021. originalTableHeight = table.style.height || ''
  1022. originalTableWidthStyle = table.style.width || ''
  1023. table.style.height = 'auto'
  1024. table.style.width = '800px'
  1025. const bodyWrapper = table.querySelector('.el-table__body-wrapper')
  1026. if (bodyWrapper) {
  1027. bodyWrapper.style.overflowX = 'visible'
  1028. bodyWrapper.style.overflowY = 'visible'
  1029. }
  1030. await new Promise(resolve => setTimeout(resolve, 200))
  1031. const canvas = await html2canvas(table, {
  1032. backgroundColor: '#ffffff',
  1033. scale: 1.5,
  1034. logging: false,
  1035. useCORS: true,
  1036. allowTaint: true,
  1037. scrollX: 0,
  1038. scrollY: 0,
  1039. width: 800,
  1040. height: table.scrollHeight
  1041. })
  1042. table.style.maxHeight = originalMaxHeight
  1043. table.style.overflowY = originalOverflowY
  1044. table.style.overflowX = originalOverflowX
  1045. table.style.width = originalTableWidth
  1046. table.style.height = originalTableHeight
  1047. table.style.width = originalTableWidthStyle
  1048. if (bodyWrapper) {
  1049. bodyWrapper.style.overflowX = ''
  1050. bodyWrapper.style.overflowY = ''
  1051. }
  1052. const imageData = canvas.toDataURL('image/png')
  1053. const base64Data = imageData.replace(/^data:image\/png;base64,/, '')
  1054. if (!base64Data || base64Data.trim() === '') {
  1055. return
  1056. }
  1057. const maxWidth = 500
  1058. const originalWidth = canvas.width
  1059. const originalHeight = canvas.height
  1060. const aspectRatio = originalWidth / originalHeight
  1061. let finalWidth = maxWidth
  1062. let finalHeight = maxWidth / aspectRatio
  1063. const maxHeight = 400
  1064. if (finalHeight > maxHeight) {
  1065. finalHeight = maxHeight
  1066. finalWidth = maxHeight * aspectRatio
  1067. }
  1068. const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))
  1069. children.push(
  1070. new Paragraph({
  1071. children: [
  1072. new ImageRun({
  1073. data: imageBytes,
  1074. transformation: {
  1075. width: Math.round(finalWidth),
  1076. height: Math.round(finalHeight)
  1077. }
  1078. })
  1079. ],
  1080. alignment: AlignmentType.CENTER,
  1081. }),
  1082. new Paragraph({ text: '' })
  1083. )
  1084. } catch (error) {
  1085. console.error('表格截图失败:', error)
  1086. }
  1087. }
  1088. onMounted(() => {
  1089. initCharts()
  1090. initTraversalCharts()
  1091. window.addEventListener('resize', handleResize)
  1092. getList()
  1093. })
  1094. // 组件卸载时移除事件监听器
  1095. onUnmounted(() => {
  1096. window.removeEventListener('resize', handleResize)
  1097. // 销毁图表实例
  1098. const charts = [
  1099. overallBarChartInstance,
  1100. participantPieChartInstance,
  1101. departmentComparisonChartInstance,
  1102. brigadePieChart1Instance,
  1103. brigadePieChart2Instance,
  1104. positionPieChart1Instance,
  1105. positionPieChart2Instance
  1106. ]
  1107. charts.forEach(chart => {
  1108. if (chart) chart.dispose()
  1109. })
  1110. })
  1111. </script>
  1112. <style lang="less" scoped>
  1113. .app-container {
  1114. padding: 20px;
  1115. }
  1116. .filter-section {
  1117. margin-bottom: 20px;
  1118. }
  1119. .filter-container {
  1120. display: flex;
  1121. justify-content: space-between;
  1122. align-items: center;
  1123. }
  1124. .search-form {
  1125. display: flex;
  1126. align-items: center;
  1127. gap: 10px;
  1128. }
  1129. .export-button {
  1130. margin-left: auto;
  1131. }
  1132. .main-section {
  1133. margin-bottom: 30px;
  1134. }
  1135. .section-title {
  1136. font-size: 20px;
  1137. font-weight: bold;
  1138. color: #333;
  1139. margin-bottom: 20px;
  1140. text-align: center;
  1141. }
  1142. .chart-row {
  1143. display: grid;
  1144. grid-template-columns: 1fr 1fr;
  1145. gap: 20px;
  1146. margin-bottom: 20px;
  1147. }
  1148. .chart-card {
  1149. height: 400px;
  1150. }
  1151. .summary-table-card {
  1152. margin-bottom: 20px;
  1153. }
  1154. .chart-header {
  1155. font-size: 16px;
  1156. font-weight: 600;
  1157. color: #333;
  1158. margin-bottom: 10px;
  1159. text-align: center;
  1160. }
  1161. .chart-container {
  1162. width: 100%;
  1163. height: 350px;
  1164. }
  1165. .pie-charts-container {
  1166. display: grid;
  1167. grid-template-columns: 1fr 1fr;
  1168. gap: 10px;
  1169. height: 350px;
  1170. }
  1171. .pie-chart {
  1172. width: 100%;
  1173. height: 100%;
  1174. }
  1175. :deep(.el-table) {
  1176. .el-table__header th {
  1177. background-color: #f5f7fa;
  1178. font-weight: 600;
  1179. }
  1180. }
  1181. /* 遍历样式 */
  1182. .traversal-section {
  1183. margin-bottom: 30px;
  1184. }
  1185. .traversal-container {
  1186. border: 1px solid #dcdfe6;
  1187. border-radius: 8px;
  1188. overflow: hidden;
  1189. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1190. margin-bottom: 20px;
  1191. }
  1192. .traversal-header {
  1193. background-color: #f5f7fa;
  1194. padding: 15px 20px;
  1195. font-size: 18px;
  1196. font-weight: 600;
  1197. color: #303133;
  1198. border-bottom: 1px solid #dcdfe6;
  1199. text-align: left;
  1200. }
  1201. .traversal-content {
  1202. display: grid;
  1203. grid-template-columns: 1fr 1fr;
  1204. gap: 20px;
  1205. padding: 20px;
  1206. }
  1207. .table-section {
  1208. min-height: 400px;
  1209. }
  1210. .chart-section {
  1211. display: grid;
  1212. grid-template-columns: 1fr 1fr;
  1213. gap: 15px;
  1214. height: 400px;
  1215. }
  1216. .pie-chart-container,
  1217. .bar-chart-container {
  1218. display: flex;
  1219. flex-direction: column;
  1220. border: 1px solid #e4e7ed;
  1221. border-radius: 6px;
  1222. padding: 10px;
  1223. background-color: #fff;
  1224. }
  1225. .pie-chart-title,
  1226. .chart-title {
  1227. font-size: 14px;
  1228. font-weight: 600;
  1229. color: #606266;
  1230. margin-bottom: 10px;
  1231. text-align: center;
  1232. }
  1233. .pie-chart,
  1234. .bar-chart {
  1235. flex: 1;
  1236. min-height: 300px;
  1237. }
  1238. /* 响应式布局 */
  1239. @media (max-width: 1200px) {
  1240. .chart-row {
  1241. grid-template-columns: 1fr;
  1242. }
  1243. .pie-charts-container {
  1244. grid-template-columns: 1fr;
  1245. }
  1246. .traversal-content {
  1247. grid-template-columns: 1fr;
  1248. gap: 15px;
  1249. }
  1250. .chart-section {
  1251. grid-template-columns: 1fr;
  1252. height: auto;
  1253. }
  1254. .pie-chart,
  1255. .bar-chart {
  1256. min-height: 250px;
  1257. }
  1258. }
  1259. @media (max-width: 768px) {
  1260. .traversal-content {
  1261. padding: 15px;
  1262. }
  1263. .chart-section {
  1264. gap: 10px;
  1265. }
  1266. .pie-chart-container,
  1267. .bar-chart-container {
  1268. padding: 8px;
  1269. }
  1270. }
  1271. </style>