performanceAnalysis.vue 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701
  1. <template>
  2. <div class="performance-analysis">
  3. <!-- 头部导航 -->
  4. <div class="header">
  5. <div class="header-left">
  6. <el-button link class="back-btn" @click="handleBack">
  7. <el-icon>
  8. <ArrowLeft />
  9. </el-icon>
  10. <span>返回AI助手</span>
  11. </el-button>
  12. </div>
  13. <!-- <div class="header-right">
  14. <el-button class="export-btn" @click="handleExport">
  15. <el-icon>
  16. <Download />
  17. </el-icon>
  18. <span>导出文档</span>
  19. </el-button>
  20. </div> -->
  21. </div>
  22. <!-- 查询表单区域 -->
  23. <div class="query-form">
  24. <div class="form-container">
  25. <div class="custom-form horizontal-form">
  26. <div class="form-row">
  27. <div class="form-item">
  28. <div class="form-label">时间范围</div>
  29. <div class="form-content">
  30. <div class="button-group">
  31. <div v-for="option in dateRangeOptions" :key="option.value" class="custom-button"
  32. :class="{ active: queryForm.dateRangeType === option.value }"
  33. @click="handleDateRangeTypeChange(option.value)">
  34. {{ option.label }}
  35. </div>
  36. </div>
  37. <div class="month-range-picker" v-if="queryForm.dateRangeType == 'CUSTOM'">
  38. <el-date-picker v-model="queryForm.startMonth" type="month" placeholder="开始月份" class="date-picker"
  39. :disabled-date="disabledStartMonth" />
  40. <span class="range-separator">至</span>
  41. <el-date-picker v-model="queryForm.endMonth" type="month" placeholder="结束月份" class="date-picker"
  42. :disabled-date="disabledEndMonth" />
  43. </div>
  44. </div>
  45. </div>
  46. <div class="form-item">
  47. <div class="form-label">绩效维度</div>
  48. <div class="form-content">
  49. <div class="button-group">
  50. <div v-for="option in performanceDimensionOptions" :key="option.value" class="custom-button"
  51. :class="{ active: queryForm.performanceDimension === option.value }"
  52. @click="queryForm.performanceDimension = option.value">
  53. {{ option.label }}
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </div>
  62. <!-- 绩效分析内容区域 -->
  63. <div class="analysis-content">
  64. <!-- 大队和科室分析 - 双列面板 -->
  65. <div class="two-panel-layout">
  66. <!-- 大队分析 -->
  67. <div class="panel-item" :class="{ 'loading-overlay': brigadeLoading }">
  68. <div class="panel-header">
  69. <h3>大队</h3>
  70. </div>
  71. <!-- 折线图 -->
  72. <div class="chart-container">
  73. <div ref="brigadeChartRef" class="echarts-chart"></div>
  74. </div>
  75. <div class="panel-header">
  76. <h3>明细</h3>
  77. <span>得分(环比)</span>
  78. </div>
  79. <!-- 明细表格 -->
  80. <div class="table-container">
  81. <el-table :data="brigadeTableData" style="width: 100%" size="small">
  82. <el-table-column prop="time" label="" width="150" />
  83. <el-table-column v-for="column in brigadeTableColumn" :key="column.column" :prop="column.column"
  84. :label="column.name" />
  85. </el-table>
  86. </div>
  87. </div>
  88. <!-- 科室分析 -->
  89. <div class="panel-item" :class="{ 'loading-overlay': departmentLoading }">
  90. <div class="panel-header">
  91. <h3>科室</h3>
  92. </div>
  93. <!-- 折线图 -->
  94. <div class="chart-container">
  95. <div ref="departmentChartRef" class="echarts-chart"></div>
  96. </div>
  97. <div class="panel-header">
  98. <h3>明细</h3>
  99. <span>得分(环比)</span>
  100. </div>
  101. <!-- 明细表格 -->
  102. <div class="table-container">
  103. <el-table :data="departmentTableData" style="width: 100%" size="small">
  104. <el-table-column prop="time" label="" width="150" />
  105. <el-table-column v-for="column in departmentTableColumn" :key="column.column" :prop="column.column"
  106. :label="column.name" />
  107. </el-table>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- 班组分析和个人分析 - 双列面板 -->
  112. <div class="two-panel-layout">
  113. <!-- 班组分析 -->
  114. <div class="panel-item" :class="{ 'loading-overlay': teamLoading }">
  115. <div class="panel-header">
  116. <h3>班组</h3>
  117. <div class="panel-tabs">
  118. <div class="button-group small">
  119. <div v-for="option in tabOptions" :key="option.value" class="custom-button"
  120. :class="{ active: teamTab === option.value }" @click="handleTeamTabChange(option.value)">
  121. {{ option.label }}
  122. </div>
  123. </div>
  124. <div class="sort-section">
  125. <span class="divider">|</span>
  126. <el-button link @click="toggleTeamSort" class="sort-btn">
  127. <img v-if="teamSortOrder === 'desc'" src="@/assets/images/down.png" alt="降序"
  128. style="width: 16px; height: 16px;" />
  129. <img v-else src="@/assets/images/up.png" alt="升序" style="width: 16px; height: 16px;" />
  130. </el-button>
  131. </div>
  132. </div>
  133. </div>
  134. <!-- 折线图 -->
  135. <div class="chart-container">
  136. <div ref="teamChartRef" class="echarts-chart"></div>
  137. </div>
  138. <div class="panel-header">
  139. <h3>明细</h3>
  140. <span>得分(环比)</span>
  141. </div>
  142. <!-- 明细表格 -->
  143. <div class="table-container">
  144. <el-table :data="teamTableData" style="width: 100%" size="small">
  145. <el-table-column prop="time" label="" width="120" />
  146. <el-table-column v-for="column in teamTableColumn" :key="column.column" :prop="column.column"
  147. :label="column.name">
  148. <template #header="{ column }">
  149. <el-tooltip :content="getTeamColumnTooltip(column.property)" placement="top">
  150. <span>{{ column.label }}</span>
  151. </el-tooltip>
  152. </template>
  153. </el-table-column>
  154. </el-table>
  155. </div>
  156. </div>
  157. <!-- 个人分析 -->
  158. <div class="panel-item" :class="{ 'loading-overlay': personalLoading }">
  159. <div class="panel-header">
  160. <h3>个人</h3>
  161. <div class="panel-tabs">
  162. <div class="button-group small">
  163. <div v-for="option in personalTabOptions" :key="option.value" class="custom-button"
  164. :class="{ active: personalTab === option.value }" @click="handlePersonalTabChange(option.value)">
  165. {{ option.label }}
  166. </div>
  167. </div>
  168. <div class="sort-section">
  169. <span class="divider">|</span>
  170. <el-button link @click="togglePersonalSort" class="sort-btn">
  171. <img v-if="personalSortOrder === 'desc'" src="@/assets/images/down.png" alt="降序"
  172. style="width: 16px; height: 16px;" />
  173. <img v-else src="@/assets/images/up.png" alt="升序" style="width: 16px; height: 16px;" />
  174. </el-button>
  175. </div>
  176. </div>
  177. </div>
  178. <!-- 折线图 -->
  179. <div class="chart-container">
  180. <div ref="personalChartRef" class="echarts-chart"></div>
  181. </div>
  182. <div class="panel-header">
  183. <h3>明细</h3>
  184. <span>得分(环比)</span>
  185. </div>
  186. <!-- 明细表格 -->
  187. <div class="table-container">
  188. <el-table :data="personalTableData" style="width: 100%" size="small">
  189. <el-table-column prop="time" label="" width="120" />
  190. <el-table-column v-for="column in personalTableColumn" :key="column.column" :prop="column.column"
  191. :label="column.name">
  192. <template #header="{ column }">
  193. <el-tooltip :content="getPersonalColumnTooltip(column.property)" placement="top">
  194. <span>{{ column.label }}</span>
  195. </el-tooltip>
  196. </template>
  197. </el-table-column>
  198. </el-table>
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. </template>
  205. <script setup>
  206. import { ref, onMounted, onUnmounted, watch } from 'vue'
  207. import { ArrowLeft, Download, Loading } from '@element-plus/icons-vue'
  208. import { ElMessage, ElLoading } from 'element-plus'
  209. import { useEcharts } from '@/hooks/chart.js'
  210. import { getCalculateByTime, getCalculateByTimeList } from '@/api/assistant/assistant.js'
  211. // 定义emit事件
  212. const emit = defineEmits(['back'])
  213. // 查询表单数据
  214. const queryForm = ref({
  215. dateRangeType: 'THREE_MONTHS',
  216. dateRange: [],
  217. startMonth: null,
  218. endMonth: null,
  219. performanceDimension: 'SCORE'
  220. })
  221. // 标签页数据
  222. const teamTab = ref('SCORE')
  223. const personalTab = ref('SCORE')
  224. // 排序状态
  225. const teamSortOrder = ref('desc') // desc: 倒序, asc: 正序
  226. const personalSortOrder = ref('desc') // desc: 倒序, asc: 正序
  227. // 加载状态
  228. const loading = ref(false)
  229. const brigadeLoading = ref(false)
  230. const departmentLoading = ref(false)
  231. const teamLoading = ref(false)
  232. const personalLoading = ref(false)
  233. // 真实数据
  234. const realData = ref({
  235. brigade: [],
  236. brigadeList: [],
  237. team: [],
  238. teamList: [],
  239. personal: [],
  240. personalList: [],
  241. department: [],
  242. departmentList: []
  243. })
  244. // 时间范围选项
  245. const dateRangeOptions = [
  246. { value: 'THREE_MONTHS', label: '近三个月' },
  247. { value: 'SIX_MONTHS', label: '近六个月' },
  248. { value: 'CUSTOM', label: '自定义' }
  249. ]
  250. // 日期限制计算属性
  251. const disabledDate = (date) => {
  252. const now = new Date()
  253. const currentYear = now.getFullYear()
  254. const currentMonth = now.getMonth()
  255. // 禁用当月的所有日期
  256. if (date.getFullYear() === currentYear && date.getMonth() === currentMonth) {
  257. return true
  258. }
  259. // 禁用未来的日期
  260. if (date.getTime() > now.getTime()) {
  261. return true
  262. }
  263. return false
  264. }
  265. // 开始月份限制计算属性
  266. const disabledStartMonth = (date) => {
  267. const now = new Date()
  268. const currentYear = now.getFullYear()
  269. const currentMonth = now.getMonth()
  270. // 禁用当月
  271. if (date.getFullYear() === currentYear && date.getMonth() === currentMonth) {
  272. return true
  273. }
  274. // 禁用未来月份
  275. if (date.getFullYear() > currentYear || (date.getFullYear() === currentYear && date.getMonth() > currentMonth)) {
  276. return true
  277. }
  278. // 如果已经选择了结束月份,开始月份不能晚于或等于结束月份
  279. if (queryForm.value.endMonth) {
  280. const endDate = new Date(queryForm.value.endMonth)
  281. if (date.getFullYear() > endDate.getFullYear() ||
  282. (date.getFullYear() === endDate.getFullYear() && date.getMonth() >= endDate.getMonth())) {
  283. return true
  284. }
  285. }
  286. return false
  287. }
  288. // 结束月份限制计算属性
  289. const disabledEndMonth = (date) => {
  290. const now = new Date()
  291. const currentYear = now.getFullYear()
  292. const currentMonth = now.getMonth()
  293. // 禁用当月
  294. if (date.getFullYear() === currentYear && date.getMonth() === currentMonth) {
  295. return true
  296. }
  297. // 禁用未来月份
  298. if (date.getFullYear() > currentYear || (date.getFullYear() === currentYear && date.getMonth() > currentMonth)) {
  299. return true
  300. }
  301. // 如果已经选择了开始月份,结束月份不能早于或等于开始月份
  302. if (queryForm.value.startMonth) {
  303. const startDate = new Date(queryForm.value.startMonth)
  304. if (date.getFullYear() < startDate.getFullYear() ||
  305. (date.getFullYear() === startDate.getFullYear() && date.getMonth() <= startDate.getMonth())) {
  306. return true
  307. }
  308. }
  309. return false
  310. }
  311. // 绩效维度选项
  312. const performanceDimensionOptions = [
  313. { value: 'SCORE', label: '总分' },
  314. { value: 'EFFICIENCY', label: '查获效率' },
  315. { value: 'QUALIFIED', label: '巡检合格率' },
  316. { value: 'TRAINING', label: '培训得分' }
  317. ]
  318. // 班组标签页选项
  319. const tabOptions = [
  320. { value: 'SCORE', label: '分数' },
  321. { value: 'PROGRESS', label: '进步' },
  322. ]
  323. // 个人标签页选项
  324. const personalTabOptions = [
  325. { value: 'SCORE', label: '分数' },
  326. { value: 'PROGRESS', label: '进步' },
  327. ]
  328. // 图表容器引用
  329. const brigadeChartRef = ref(null)
  330. const departmentChartRef = ref(null)
  331. const teamChartRef = ref(null)
  332. const personalChartRef = ref(null)
  333. // 图表实例
  334. const { setOption: setBrigadeOption, dispose: disposeBrigade } = useEcharts(brigadeChartRef)
  335. const { setOption: setDepartmentOption, dispose: disposeDepartment } = useEcharts(departmentChartRef)
  336. const { setOption: setteamListOption, dispose: disposeTeam } = useEcharts(teamChartRef)
  337. const { setOption: setPersonalOption, dispose: disposePersonal } = useEcharts(personalChartRef)
  338. // 颜色数组
  339. const colorList = ['#6BA364', '#3AACFF', '#EABF5A', '#557DDB', '#71C5D1']
  340. // 监听时间范围和绩效维度的改变
  341. watch([() => queryForm.value.dateRangeType, () => queryForm.value.startMonth, () => queryForm.value.endMonth, () => queryForm.value.performanceDimension], () => {
  342. fetchData()
  343. })
  344. // 表格数据
  345. const brigadeTableData = ref([
  346. ])
  347. const departmentTableData = ref([
  348. ])
  349. const teamTableData = ref([
  350. ])
  351. const personalTableData = ref([
  352. ])
  353. // 大队图表配置
  354. const brigadeChartOptions = {
  355. tooltip: {
  356. trigger: 'axis'
  357. },
  358. legend: {
  359. show: true,
  360. icon: 'rect',
  361. },
  362. grid: {
  363. left: '3%',
  364. right: '4%',
  365. bottom: '3%',
  366. containLabel: true
  367. },
  368. xAxis: {
  369. type: 'category',
  370. boundaryGap: false,
  371. data: []
  372. },
  373. yAxis: {
  374. type: 'value',
  375. name: '得分',
  376. min: 80,
  377. interval: 5
  378. },
  379. series: [
  380. ]
  381. }
  382. // 科室图表配置
  383. const departmentChartOptions = {
  384. tooltip: {
  385. trigger: 'axis'
  386. },
  387. legend: {
  388. show: true,
  389. icon: 'rect',
  390. },
  391. grid: {
  392. left: '3%',
  393. right: '4%',
  394. bottom: '3%',
  395. containLabel: true
  396. },
  397. xAxis: {
  398. type: 'category',
  399. boundaryGap: false,
  400. data: []
  401. },
  402. yAxis: {
  403. type: 'value',
  404. name: '得分',
  405. min: 80,
  406. interval: 5
  407. },
  408. series: [
  409. ]
  410. }
  411. // 班组明细图表配置
  412. const teamListChartOptions = {
  413. tooltip: {
  414. trigger: 'axis'
  415. },
  416. legend: {
  417. show: true,
  418. icon: 'rect',
  419. },
  420. grid: {
  421. left: '3%',
  422. right: '4%',
  423. bottom: '3%',
  424. containLabel: true
  425. },
  426. xAxis: {
  427. type: 'category',
  428. boundaryGap: false,
  429. data: []
  430. },
  431. yAxis: {
  432. type: 'value',
  433. name: '得分',
  434. min: 80,
  435. interval: 5
  436. },
  437. series: []
  438. }
  439. // 个人图表配置
  440. const personalChartOptions = {
  441. tooltip: {
  442. trigger: 'axis'
  443. },
  444. legend: {
  445. show: true,
  446. icon: 'rect',
  447. },
  448. grid: {
  449. left: '3%',
  450. right: '4%',
  451. bottom: '3%',
  452. containLabel: true
  453. },
  454. xAxis: {
  455. type: 'category',
  456. boundaryGap: false,
  457. data: []
  458. },
  459. yAxis: {
  460. type: 'value',
  461. name: '得分',
  462. min: 80,
  463. interval: 5
  464. },
  465. series: []
  466. }
  467. // 返回按钮点击事件
  468. const handleBack = () => {
  469. emit('back')
  470. }
  471. // 计算预设时间范围的开始月和结束月
  472. const calculatePresetDateRange = (dateRangeType) => {
  473. const endDate = new Date()
  474. endDate.setDate(0) // 设置为上个月最后一天,不包括本月
  475. let startDate = new Date(endDate)
  476. if (dateRangeType === 'THREE_MONTHS') {
  477. startDate.setMonth(endDate.getMonth() - 2)
  478. } else if (dateRangeType === 'SIX_MONTHS') {
  479. startDate.setMonth(endDate.getMonth() - 5)
  480. }
  481. console.log(new Date(startDate.getFullYear(), startDate.getMonth(), 1), 2222)
  482. // 返回开始月和结束月的月份对象
  483. return {
  484. startMonth: new Date(startDate.getFullYear(), startDate.getMonth(), 1),
  485. endMonth: new Date(endDate.getFullYear(), endDate.getMonth(), 1)
  486. }
  487. }
  488. // 安全的日期格式化函数
  489. const formatDate = (date) => {
  490. const year = date.getFullYear()
  491. const month = String(date.getMonth() + 1).padStart(2, '0')
  492. const day = String(date.getDate()).padStart(2, '0')
  493. return `${year}-${month}-${day}`
  494. }
  495. // 获取班组表格列头的tooltip内容
  496. const getTeamColumnTooltip = (columnProperty) => {
  497. // 查找当前行的数据,获取对应的科室名称
  498. const currentRow = teamTableData.value[0] // 取第一行数据作为参考
  499. if (currentRow && currentRow[`${columnProperty}DeptName`]) {
  500. return currentRow[`${columnProperty}DeptName`]
  501. }
  502. return ''
  503. }
  504. // 获取个人表格列头的tooltip内容
  505. const getPersonalColumnTooltip = (columnProperty) => {
  506. // 查找当前行的数据,获取对应的className
  507. const currentRow = personalTableData.value[0] // 取第一行数据作为参考
  508. if (currentRow && currentRow[`${columnProperty}ClassName`] && currentRow && currentRow[`${columnProperty}DeptName`]) {
  509. return `${currentRow[`${columnProperty}DeptName`]}/${currentRow[`${columnProperty}ClassName`]}`
  510. }
  511. return ''
  512. }
  513. // 处理时间范围类型切换
  514. const handleDateRangeTypeChange = (dateRangeType) => {
  515. // 如果切换到自定义时间范围,并且之前是预设时间范围,则自动填充开始月和结束月
  516. if (dateRangeType === 'CUSTOM' && (queryForm.value.dateRangeType === 'THREE_MONTHS' || queryForm.value.dateRangeType === 'SIX_MONTHS')) {
  517. const presetRange = calculatePresetDateRange(queryForm.value.dateRangeType)
  518. queryForm.value.startMonth = presetRange.startMonth
  519. queryForm.value.endMonth = presetRange.endMonth
  520. }
  521. // 更新时间范围类型
  522. queryForm.value.dateRangeType = dateRangeType
  523. }
  524. // 绩效维度映射
  525. const performanceDimensionMap = {
  526. 'SCORE': 'totalScore',
  527. 'EFFICIENCY': 'seizureEfficiency',
  528. 'QUALIFIED': 'inspectionPassRate',
  529. 'TRAINING': 'trainingScore'
  530. }
  531. // 维度类型映射
  532. const dimensionTypeMap = {
  533. 'SCORE': 1,
  534. 'PROGRESS': 2
  535. }
  536. // 排序映射
  537. const sortOrderMap = {
  538. 'desc': 2,
  539. 'asc': 1
  540. }
  541. // 构建科室API参数 (没有dimensionType参数)
  542. const buildDepartmentApiParams = () => {
  543. const params = {
  544. resultField: performanceDimensionMap[queryForm.value.performanceDimension],
  545. sortOrder: 1
  546. }
  547. // 处理时间范围
  548. if (queryForm.value.dateRangeType === 'CUSTOM' && queryForm.value.startMonth && queryForm.value.endMonth) {
  549. // 处理自定义月份范围选择
  550. const startDate = new Date(queryForm.value.startMonth)
  551. const endDate = new Date(queryForm.value.endMonth)
  552. console.log(queryForm.value.startMonth, queryForm.value.endMonth, startDate, endDate, startDate.getFullYear(), startDate.getMonth())
  553. // debugger
  554. // 开始月的第一天
  555. const startTime = new Date(startDate.getFullYear(), startDate.getMonth(), 1)
  556. // 结束月的最后一天
  557. const endTime = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)
  558. params.startTime = formatDate(startTime)
  559. params.endTime = formatDate(endTime)
  560. } else {
  561. // 处理预设时间范围
  562. const endDate = new Date()
  563. endDate.setDate(0) // 设置为上个月最后一天,不包括本月
  564. let startDate = new Date(endDate)
  565. if (queryForm.value.dateRangeType === 'THREE_MONTHS') {
  566. startDate.setMonth(endDate.getMonth() - 2)
  567. } else if (queryForm.value.dateRangeType === 'SIX_MONTHS') {
  568. startDate.setMonth(endDate.getMonth() - 5)
  569. }
  570. params.startTime = formatDate(startDate)
  571. params.endTime = formatDate(endDate)
  572. }
  573. return params
  574. }
  575. // 构建班组API参数 (包含dimensionType和sortOrder)
  576. const buildTeamApiParams = () => {
  577. const params = {
  578. resultField: performanceDimensionMap[queryForm.value.performanceDimension],
  579. sortOrder: sortOrderMap[teamSortOrder.value],
  580. dimensionType: dimensionTypeMap[teamTab.value]
  581. }
  582. // 处理时间范围
  583. if (queryForm.value.dateRangeType === 'CUSTOM' && queryForm.value.startMonth && queryForm.value.endMonth) {
  584. // 处理自定义月份范围选择
  585. const startDate = new Date(queryForm.value.startMonth)
  586. const endDate = new Date(queryForm.value.endMonth)
  587. // 开始月的第一天
  588. const startTime = new Date(startDate.getFullYear(), startDate.getMonth(), 1)
  589. // 结束月的最后一天
  590. const endTime = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)
  591. params.startTime = formatDate(startTime)
  592. params.endTime = formatDate(endTime)
  593. } else {
  594. // 处理预设时间范围
  595. const endDate = new Date()
  596. endDate.setDate(0) // 设置为上个月最后一天,不包括本月
  597. let startDate = new Date(endDate)
  598. if (queryForm.value.dateRangeType === 'THREE_MONTHS') {
  599. startDate.setMonth(endDate.getMonth() - 2)
  600. } else if (queryForm.value.dateRangeType === 'SIX_MONTHS') {
  601. startDate.setMonth(endDate.getMonth() - 5)
  602. }
  603. // 开始时间增加1天
  604. startDate.setDate(startDate.getDate() + 1)
  605. params.startTime = formatDate(startDate)
  606. params.endTime = formatDate(endDate)
  607. }
  608. return params
  609. }
  610. // 构建个人API参数 (包含dimensionType和sortOrder)
  611. const buildPersonalApiParams = () => {
  612. const params = {
  613. resultField: performanceDimensionMap[queryForm.value.performanceDimension],
  614. sortOrder: sortOrderMap[personalSortOrder.value],
  615. dimensionType: dimensionTypeMap[personalTab.value]
  616. }
  617. // 处理时间范围
  618. if (queryForm.value.dateRangeType === 'CUSTOM' && queryForm.value.startMonth && queryForm.value.endMonth) {
  619. // 处理自定义月份范围选择
  620. const startDate = new Date(queryForm.value.startMonth)
  621. const endDate = new Date(queryForm.value.endMonth)
  622. // 开始月的第一天
  623. const startTime = new Date(startDate.getFullYear(), startDate.getMonth(), 1)
  624. // 结束月的最后一天
  625. const endTime = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)
  626. params.startTime = formatDate(startTime)
  627. params.endTime = formatDate(endTime)
  628. } else {
  629. // 处理预设时间范围
  630. const endDate = new Date()
  631. endDate.setDate(0) // 设置为上个月最后一天,不包括本月
  632. let startDate = new Date(endDate)
  633. if (queryForm.value.dateRangeType === 'THREE_MONTHS') {
  634. startDate.setMonth(endDate.getMonth() - 2)
  635. } else if (queryForm.value.dateRangeType === 'SIX_MONTHS') {
  636. startDate.setMonth(endDate.getMonth() - 5)
  637. }
  638. // 开始时间增加1天
  639. startDate.setDate(startDate.getDate() + 1)
  640. params.startTime = formatDate(startDate)
  641. params.endTime = formatDate(endDate)
  642. }
  643. return params
  644. }
  645. // 构建大队API参数 (dimension = 4,没有dimensionType参数)
  646. const buildBrigadeApiParams = () => {
  647. const params = {
  648. resultField: performanceDimensionMap[queryForm.value.performanceDimension],
  649. sortOrder: 1
  650. }
  651. // 处理时间范围
  652. if (queryForm.value.dateRangeType === 'CUSTOM' && queryForm.value.startMonth && queryForm.value.endMonth) {
  653. // 处理自定义月份范围选择
  654. const startDate = new Date(queryForm.value.startMonth)
  655. const endDate = new Date(queryForm.value.endMonth)
  656. // 开始月的第一天
  657. const startTime = new Date(startDate.getFullYear(), startDate.getMonth(), 1)
  658. // 结束月的最后一天
  659. const endTime = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)
  660. params.startTime = formatDate(startTime)
  661. params.endTime = formatDate(endTime)
  662. } else {
  663. // 处理预设时间范围
  664. const endDate = new Date()
  665. endDate.setDate(0) // 设置为上个月最后一天,不包括本月
  666. let startDate = new Date(endDate)
  667. if (queryForm.value.dateRangeType === 'THREE_MONTHS') {
  668. startDate.setMonth(endDate.getMonth() - 2)
  669. } else if (queryForm.value.dateRangeType === 'SIX_MONTHS') {
  670. startDate.setMonth(endDate.getMonth() - 5)
  671. }
  672. params.startTime = formatDate(startDate)
  673. params.endTime = formatDate(endDate)
  674. }
  675. return params
  676. }
  677. // 数据请求函数
  678. // 获取大队数据
  679. const fetchBrigadeData = async () => {
  680. brigadeLoading.value = true
  681. try {
  682. // 大队数据 (dimension = 4,没有dimensionType参数)
  683. const brigadeChartParams = buildBrigadeApiParams()
  684. brigadeChartParams.dimension = 4
  685. const brigadeTableParams = buildBrigadeApiParams()
  686. brigadeTableParams.dimension = 4
  687. // 并行请求大队图表和表格数据
  688. const [brigadeChartResponse, brigadeTableResponse] = await Promise.all([
  689. getCalculateByTime(brigadeChartParams),
  690. getCalculateByTimeList(brigadeTableParams)
  691. ])
  692. // 设置大队数据
  693. if (brigadeChartResponse.data) {
  694. realData.value.brigade = brigadeChartResponse.data || []
  695. }
  696. if (brigadeTableResponse.data) {
  697. realData.value.brigadeList = brigadeTableResponse.data || []
  698. }
  699. // 请求完成后直接更新大队数据
  700. updateBrigadeData()
  701. } catch (error) {
  702. console.error('大队数据请求失败:', error)
  703. ElMessage.error('大队数据加载失败')
  704. } finally {
  705. brigadeLoading.value = false
  706. }
  707. }
  708. // 获取科室数据
  709. const fetchDepartmentData = async () => {
  710. departmentLoading.value = true
  711. try {
  712. // 科室数据 (dimension = 3,没有dimensionType参数)
  713. const departmentChartParams = buildDepartmentApiParams()
  714. departmentChartParams.dimension = 3
  715. const departmentTableParams = buildDepartmentApiParams()
  716. departmentTableParams.dimension = 3
  717. // 并行请求科室图表和表格数据
  718. const [departmentChartResponse, departmentTableResponse] = await Promise.all([
  719. getCalculateByTime(departmentChartParams),
  720. getCalculateByTimeList(departmentTableParams)
  721. ])
  722. // 设置科室数据
  723. if (departmentChartResponse.data) {
  724. realData.value.department = departmentChartResponse.data || []
  725. }
  726. if (departmentTableResponse.data) {
  727. realData.value.departmentList = departmentTableResponse.data || []
  728. }
  729. // 请求完成后直接更新科室数据
  730. updateDepartmentData()
  731. } catch (error) {
  732. console.error('科室数据请求失败:', error)
  733. ElMessage.error('科室数据加载失败')
  734. } finally {
  735. departmentLoading.value = false
  736. }
  737. }
  738. // 获取班组数据
  739. const fetchTeamData = async () => {
  740. teamLoading.value = true
  741. try {
  742. // 班组数据 (dimension = 2,包含dimensionType和sortOrder)
  743. const teamChartParams = buildTeamApiParams()
  744. teamChartParams.dimension = 2
  745. const teamTableParams = buildTeamApiParams()
  746. teamTableParams.dimension = 2
  747. // 并行请求班组图表和表格数据
  748. const [teamChartResponse, teamTableResponse] = await Promise.all([
  749. getCalculateByTime(teamChartParams),
  750. getCalculateByTimeList(teamTableParams)
  751. ])
  752. // 设置班组数据
  753. if (teamChartResponse.data) {
  754. realData.value.team = teamChartResponse.data || []
  755. }
  756. if (teamTableResponse.data) {
  757. realData.value.teamList = teamTableResponse.data || []
  758. }
  759. // 请求完成后直接更新班组数据
  760. updateTeamData()
  761. } catch (error) {
  762. console.error('班组数据请求失败:', error)
  763. ElMessage.error('班组数据加载失败')
  764. } finally {
  765. teamLoading.value = false
  766. }
  767. }
  768. // 获取个人数据
  769. const fetchPersonalData = async () => {
  770. personalLoading.value = true
  771. try {
  772. // 个人数据 (dimension = 1,包含dimensionType和sortOrder)
  773. const personalChartParams = buildPersonalApiParams()
  774. personalChartParams.dimension = 1
  775. const personalTableParams = buildPersonalApiParams()
  776. personalTableParams.dimension = 1
  777. // 并行请求个人图表和表格数据
  778. const [personalChartResponse, personalTableResponse] = await Promise.all([
  779. getCalculateByTime(personalChartParams),
  780. getCalculateByTimeList(personalTableParams)
  781. ])
  782. // 设置个人数据
  783. if (personalChartResponse.data) {
  784. realData.value.personal = personalChartResponse.data || []
  785. }
  786. if (personalTableResponse.data) {
  787. realData.value.personalList = personalTableResponse.data || []
  788. }
  789. // 请求完成后直接更新个人数据
  790. updatePersonalData()
  791. } catch (error) {
  792. console.error('个人数据请求失败:', error)
  793. ElMessage.error('个人数据加载失败')
  794. } finally {
  795. personalLoading.value = false
  796. }
  797. }
  798. const fetchData = async () => {
  799. loading.value = true
  800. try {
  801. // 并行请求大队、科室、班组和个人数据
  802. // 数据更新已经在各个数据请求函数中完成
  803. await Promise.all([
  804. fetchBrigadeData(),
  805. fetchDepartmentData(),
  806. fetchTeamData(),
  807. fetchPersonalData()
  808. ])
  809. } catch (error) {
  810. console.error('数据请求失败:', error)
  811. ElMessage.error('数据加载失败')
  812. } finally {
  813. loading.value = false
  814. }
  815. }
  816. // 表格列定义
  817. const brigadeTableColumn = ref([])
  818. const departmentTableColumn = ref([])
  819. const teamTableColumn = ref([])
  820. const personalTableColumn = ref([])
  821. const getColumnData = (arr, column) => {
  822. let res = arr.map(item => {
  823. const { dataItems } = item;
  824. let items = [...dataItems];
  825. let findAll = items.map(ele => {
  826. let getColumn = column.find(e => e.name === ele.name)
  827. return {
  828. ...ele,
  829. [getColumn.column]: ele.displayValue,
  830. [`${getColumn.column}DeptName`]: ele?.deptName || '',
  831. [`${getColumn.column}ClassName`]: ele?.className || ''
  832. }
  833. })
  834. // 将对象数组转换为扁平的对象
  835. const flattenedObject = findAll.reduce((acc, obj) => {
  836. return { ...acc, ...obj };
  837. }, {});
  838. return {
  839. time: item.timeLabel,
  840. ...flattenedObject
  841. }
  842. })
  843. return res
  844. }
  845. // 更新大队数据(包含图表和表格)
  846. const updateBrigadeData = () => {
  847. // 更新大队图表
  848. if (realData.value.brigade) {
  849. const xAxisData = realData.value.brigade.timeAxis;
  850. let seriesData = [];
  851. let timeDataList = realData.value.brigade.timeDataList;
  852. let arr = {};
  853. timeDataList.forEach(item => {
  854. item.dataItems.forEach(ele => {
  855. if (!arr[ele.name]) {
  856. arr[ele.name] = [];
  857. }
  858. arr[ele.name].push(ele.value);
  859. })
  860. })
  861. Object.keys(arr).forEach((item, index) => {
  862. seriesData.push({
  863. name: item,
  864. type: 'line',
  865. symbol: 'circle',
  866. symbolSize: 6,
  867. data: arr[item],
  868. itemStyle: { color: colorList[index % colorList.length] },
  869. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  870. })
  871. })
  872. setBrigadeOption({
  873. ...brigadeChartOptions,
  874. xAxis: { ...brigadeChartOptions.xAxis, data: xAxisData },
  875. series: seriesData
  876. })
  877. }
  878. // 更新大队表格数据
  879. if (realData.value.brigadeList) {
  880. const timeDataList = realData.value.brigadeList.timeDataList;
  881. brigadeTableColumn.value = []
  882. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  883. timeDataList[0].dataItems.forEach((item, index) => {
  884. brigadeTableColumn.value.push({ name: item.name, column: `brigade${index}` })
  885. })
  886. }
  887. brigadeTableData.value = getColumnData(timeDataList, brigadeTableColumn.value)
  888. }
  889. }
  890. // 更新科室数据(包含图表和表格)
  891. const updateDepartmentData = () => {
  892. // 更新科室图表
  893. if (realData.value.department) {
  894. const xAxisData = realData.value.department.timeAxis;
  895. let seriesData = [];
  896. let timeDataList = realData.value.department.timeDataList;
  897. let arr = {};
  898. timeDataList.forEach(item => {
  899. item.dataItems.forEach(ele => {
  900. if (!arr[ele.name]) {
  901. arr[ele.name] = [];
  902. }
  903. arr[ele.name].push(ele.value);
  904. })
  905. })
  906. Object.keys(arr).forEach((item, index) => {
  907. seriesData.push({
  908. name: item,
  909. type: 'line',
  910. symbol: 'circle',
  911. symbolSize: 6,
  912. data: arr[item],
  913. itemStyle: { color: colorList[index % colorList.length] },
  914. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  915. })
  916. })
  917. setDepartmentOption({
  918. ...departmentChartOptions,
  919. xAxis: { ...departmentChartOptions.xAxis, data: xAxisData },
  920. series: seriesData
  921. })
  922. }
  923. // 更新科室表格数据
  924. if (realData.value.departmentList) {
  925. console.log(realData.value.departmentList, 'realData.value.departmentList');
  926. const timeDataList = realData.value.departmentList.timeDataList;
  927. departmentTableColumn.value = []
  928. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  929. timeDataList[0].dataItems.forEach((item, index) => {
  930. departmentTableColumn.value.push({ name: item.name, column: `depart${index}` })
  931. })
  932. }
  933. departmentTableData.value = getColumnData(timeDataList, departmentTableColumn.value)
  934. console.log(departmentTableData.value, 'departmentTableData');
  935. }
  936. }
  937. // 更新班组数据(包含图表和表格)
  938. const updateTeamData = () => {
  939. // 更新班组图表
  940. if (realData.value.team && realData.value.team.timeDataList) {
  941. const xAxisData = realData.value.team.timeAxis;
  942. let seriesData = [];
  943. let timeDataList = realData.value.team.timeDataList;
  944. let arr = {};
  945. timeDataList.forEach(item => {
  946. item.dataItems.forEach(ele => {
  947. if (!arr[ele.name]) {
  948. arr[ele.name] = [];
  949. }
  950. arr[ele.name].push(ele.value);
  951. })
  952. })
  953. Object.keys(arr).forEach((item, index) => {
  954. seriesData.push({
  955. name: item,
  956. type: 'line',
  957. symbol: 'circle',
  958. symbolSize: 6,
  959. data: arr[item],
  960. itemStyle: { color: colorList[index % colorList.length] },
  961. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  962. })
  963. })
  964. setteamListOption({
  965. ...teamListChartOptions,
  966. xAxis: { ...teamListChartOptions.xAxis, data: xAxisData },
  967. series: seriesData
  968. })
  969. }
  970. // 更新班组表格数据
  971. if (realData.value.teamList) {
  972. const timeDataList = realData.value.teamList.timeDataList;
  973. teamTableColumn.value = []
  974. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  975. timeDataList[0].dataItems.forEach((item, index) => {
  976. teamTableColumn.value.push({ name: item.name, column: `team${index}` })
  977. })
  978. }
  979. teamTableData.value = getColumnData(timeDataList, teamTableColumn.value)
  980. console.log(teamTableData.value, 'teamTableData');
  981. }
  982. }
  983. // 更新个人数据(包含图表和表格)
  984. const updatePersonalData = () => {
  985. // 更新个人图表
  986. if (realData.value.personal && realData.value.personal.timeDataList) {
  987. const xAxisData = realData.value.personal.timeAxis;
  988. let seriesData = [];
  989. let timeDataList = realData.value.personal.timeDataList;
  990. let arr = {};
  991. timeDataList.forEach(item => {
  992. item.dataItems.forEach(ele => {
  993. if (!arr[ele.name]) {
  994. arr[ele.name] = [];
  995. }
  996. arr[ele.name].push(ele.value);
  997. })
  998. })
  999. Object.keys(arr).forEach((item, index) => {
  1000. seriesData.push({
  1001. name: item,
  1002. type: 'line',
  1003. symbol: 'circle',
  1004. symbolSize: 6,
  1005. data: arr[item],
  1006. itemStyle: { color: colorList[index % colorList.length] },
  1007. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  1008. })
  1009. })
  1010. setPersonalOption({
  1011. ...personalChartOptions,
  1012. xAxis: { ...personalChartOptions.xAxis, data: xAxisData },
  1013. series: seriesData
  1014. })
  1015. }
  1016. // 更新个人表格数据
  1017. if (realData.value.personalList) {
  1018. const timeDataList = realData.value.personalList.timeDataList;
  1019. personalTableColumn.value = []
  1020. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  1021. timeDataList[0].dataItems.forEach((item, index) => {
  1022. personalTableColumn.value.push({ name: item.name, column: `personal${index}` })
  1023. })
  1024. }
  1025. personalTableData.value = getColumnData(timeDataList, personalTableColumn.value)
  1026. }
  1027. }
  1028. // 更新班组图表数据
  1029. const updateTeamChartData = () => {
  1030. if (realData.value.team && realData.value.team.timeDataList) {
  1031. const xAxisData = realData.value.team.timeAxis;
  1032. let seriesData = [];
  1033. let timeDataList = realData.value.team.timeDataList;
  1034. let arr = {};
  1035. timeDataList.forEach(item => {
  1036. item.dataItems.forEach(ele => {
  1037. if (!arr[ele.name]) {
  1038. arr[ele.name] = [];
  1039. }
  1040. arr[ele.name].push(ele.value);
  1041. })
  1042. })
  1043. Object.keys(arr).forEach((item, index) => {
  1044. seriesData.push({
  1045. name: item,
  1046. type: 'line',
  1047. symbol: 'circle',
  1048. symbolSize: 6,
  1049. data: arr[item],
  1050. itemStyle: { color: colorList[index % colorList.length] },
  1051. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  1052. })
  1053. })
  1054. setteamListOption({
  1055. ...teamListChartOptions,
  1056. xAxis: { ...teamListChartOptions.xAxis, data: xAxisData },
  1057. series: seriesData
  1058. })
  1059. }
  1060. }
  1061. // 更新个人图表数据
  1062. const updatePersonalChartData = () => {
  1063. if (realData.value.personal && realData.value.personal.timeDataList) {
  1064. const xAxisData = realData.value.personal.timeAxis;
  1065. let seriesData = [];
  1066. let timeDataList = realData.value.personal.timeDataList;
  1067. let arr = {};
  1068. timeDataList.forEach(item => {
  1069. item.dataItems.forEach(ele => {
  1070. if (!arr[ele.name]) {
  1071. arr[ele.name] = [];
  1072. }
  1073. arr[ele.name].push(ele.value);
  1074. })
  1075. })
  1076. Object.keys(arr).forEach((item, index) => {
  1077. seriesData.push({
  1078. name: item,
  1079. type: 'line',
  1080. symbol: 'circle',
  1081. symbolSize: 6,
  1082. data: arr[item],
  1083. itemStyle: { color: colorList[index % colorList.length] },
  1084. lineStyle: { color: colorList[index % colorList.length], width: 2 }
  1085. })
  1086. })
  1087. setPersonalOption({
  1088. ...personalChartOptions,
  1089. xAxis: { ...personalChartOptions.xAxis, data: xAxisData },
  1090. series: seriesData
  1091. })
  1092. }
  1093. }
  1094. // 更新班组表格数据
  1095. const updateTeamTableData = () => {
  1096. if (realData.value.teamList) {
  1097. const timeDataList = realData.value.teamList.timeDataList;
  1098. teamTableColumn.value = []
  1099. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  1100. timeDataList[0].dataItems.forEach((item, index) => {
  1101. teamTableColumn.value.push({ name: item.name, column: `team${index}` })
  1102. })
  1103. }
  1104. teamTableData.value = getColumnData(timeDataList, teamTableColumn.value)
  1105. }
  1106. }
  1107. // 更新个人表格数据
  1108. const updatePersonalTableData = () => {
  1109. if (realData.value.personalList) {
  1110. const timeDataList = realData.value.personalList.timeDataList;
  1111. personalTableColumn.value = []
  1112. if (timeDataList && timeDataList.length > 0 && timeDataList[0].dataItems) {
  1113. timeDataList[0].dataItems.forEach((item, index) => {
  1114. personalTableColumn.value.push({ name: item.name, column: `personal${index}` })
  1115. })
  1116. }
  1117. personalTableData.value = getColumnData(timeDataList, personalTableColumn.value)
  1118. }
  1119. }
  1120. // 标签页切换处理
  1121. const handleTeamTabChange = async (value) => {
  1122. teamTab.value = value
  1123. teamLoading.value = true
  1124. try {
  1125. await fetchTeamData()
  1126. updateTeamData()
  1127. } catch (error) {
  1128. console.error('班组标签页切换失败:', error)
  1129. ElMessage.error('班组数据加载失败')
  1130. } finally {
  1131. teamLoading.value = false
  1132. }
  1133. }
  1134. const handlePersonalTabChange = async (value) => {
  1135. personalTab.value = value
  1136. personalLoading.value = true
  1137. try {
  1138. await fetchPersonalData()
  1139. updatePersonalData()
  1140. } catch (error) {
  1141. console.error('个人标签页切换失败:', error)
  1142. ElMessage.error('个人数据加载失败')
  1143. } finally {
  1144. personalLoading.value = false
  1145. }
  1146. }
  1147. // 切换排序顺序
  1148. const toggleTeamSort = async () => {
  1149. teamSortOrder.value = teamSortOrder.value === 'desc' ? 'asc' : 'desc'
  1150. teamLoading.value = true
  1151. try {
  1152. await fetchTeamData()
  1153. updateTeamData()
  1154. } catch (error) {
  1155. console.error('班组排序切换失败:', error)
  1156. ElMessage.error('班组数据加载失败')
  1157. } finally {
  1158. teamLoading.value = false
  1159. }
  1160. }
  1161. const togglePersonalSort = async () => {
  1162. personalSortOrder.value = personalSortOrder.value === 'desc' ? 'asc' : 'desc'
  1163. personalLoading.value = true
  1164. try {
  1165. await fetchPersonalData()
  1166. updatePersonalData()
  1167. } catch (error) {
  1168. console.error('个人排序切换失败:', error)
  1169. ElMessage.error('个人数据加载失败')
  1170. } finally {
  1171. personalLoading.value = false
  1172. }
  1173. }
  1174. // 初始化图表
  1175. const initCharts = () => {
  1176. if (teamChartRef.value && teamChartRef.value.offsetHeight > 0) {
  1177. setDepartmentOption(departmentChartOptions)
  1178. }
  1179. if (teamChartRef.value && teamChartRef.value.offsetHeight > 0) {
  1180. setteamListOption(teamListChartOptions)
  1181. }
  1182. if (personalChartRef.value && personalChartRef.value.offsetHeight > 0) {
  1183. setPersonalOption(personalChartOptions)
  1184. }
  1185. }
  1186. // 组件挂载
  1187. onMounted(() => {
  1188. initCharts()
  1189. setTimeout(() => {
  1190. initCharts()
  1191. }, 200)
  1192. // 初始加载数据
  1193. fetchData()
  1194. })
  1195. // 组件卸载时销毁图表
  1196. onUnmounted(() => {
  1197. disposeBrigade()
  1198. disposeDepartment()
  1199. disposeTeam()
  1200. disposePersonal()
  1201. })
  1202. </script>
  1203. <style lang="scss" scoped>
  1204. .littleTitle {
  1205. font-size: 16px;
  1206. font-weight: 600;
  1207. color: #333;
  1208. margin: 0;
  1209. }
  1210. .performance-analysis {
  1211. width: 100%;
  1212. min-height: 100vh;
  1213. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  1214. padding: 20px;
  1215. }
  1216. .header {
  1217. display: flex;
  1218. justify-content: space-between;
  1219. align-items: center;
  1220. padding: 0 16px 16px 16px;
  1221. }
  1222. .back-btn {
  1223. color: #3D3D3D;
  1224. font-size: 14px;
  1225. }
  1226. .back-btn:hover {
  1227. color: #3D3D3D;
  1228. }
  1229. .export-btn {
  1230. background-color: transparent;
  1231. }
  1232. .export-btn:hover {
  1233. background-color: rgba(0, 0, 0, 0.05);
  1234. }
  1235. .query-form {
  1236. background: white;
  1237. border-radius: 8px;
  1238. padding: 12px 18px;
  1239. margin-bottom: 16px;
  1240. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1241. }
  1242. .form-container {
  1243. width: 60%;
  1244. }
  1245. .custom-form .form-row {
  1246. display: flex;
  1247. align-items: flex-start;
  1248. gap: 40px;
  1249. }
  1250. .custom-form .form-item {
  1251. display: flex;
  1252. align-items: center;
  1253. gap: 6px;
  1254. flex: 1;
  1255. }
  1256. .custom-form .form-label {
  1257. width: 80px;
  1258. font-size: 14px;
  1259. color: #3D3D3D;
  1260. font-size: 15px;
  1261. text-align: left;
  1262. flex-shrink: 0;
  1263. }
  1264. .form-content {
  1265. display: flex;
  1266. align-items: center;
  1267. gap: 16px;
  1268. flex: 1;
  1269. }
  1270. .button-group {
  1271. display: flex;
  1272. gap: 8px;
  1273. }
  1274. .button-group.small {
  1275. gap: 4px;
  1276. }
  1277. .custom-button {
  1278. padding: 8px 16px;
  1279. border: 1px solid #2879FF;
  1280. border-radius: 4px;
  1281. background-color: #FFFFFF;
  1282. color: #2879FF;
  1283. font-size: 14px;
  1284. cursor: pointer;
  1285. transition: all 0.3s ease;
  1286. text-align: center;
  1287. user-select: none;
  1288. }
  1289. .button-group.small .custom-button {
  1290. padding: 4px 12px;
  1291. font-size: 12px;
  1292. }
  1293. .custom-button:hover {
  1294. background-color: #f0f7ff;
  1295. }
  1296. .custom-button.active {
  1297. background-color: #2879FF;
  1298. color: #FFFFFF;
  1299. }
  1300. .custom-button.active:hover {
  1301. background-color: #1e6fd9;
  1302. }
  1303. .date-picker {
  1304. min-width: 300px;
  1305. }
  1306. .month-range-picker {
  1307. display: flex;
  1308. align-items: center;
  1309. gap: 8px;
  1310. }
  1311. .month-range-picker .date-picker {
  1312. min-width: 140px;
  1313. }
  1314. .range-separator {
  1315. color: #606266;
  1316. font-size: 14px;
  1317. white-space: nowrap;
  1318. }
  1319. .analysis-content {
  1320. display: flex;
  1321. flex-direction: column;
  1322. gap: 16px;
  1323. }
  1324. .two-panel-layout {
  1325. display: flex;
  1326. gap: 16px;
  1327. }
  1328. .panel-item {
  1329. flex: 1;
  1330. background: white;
  1331. border-radius: 8px;
  1332. padding: 20px;
  1333. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1334. display: flex;
  1335. flex-direction: column;
  1336. gap: 16px;
  1337. }
  1338. .panel-item.full-width {
  1339. width: 100%;
  1340. }
  1341. .panel-header {
  1342. display: flex;
  1343. justify-content: space-between;
  1344. align-items: center;
  1345. padding-bottom: 0;
  1346. span {
  1347. color: #666666;
  1348. font-size: 13px;
  1349. }
  1350. }
  1351. .panel-header h3 {
  1352. font-size: 16px;
  1353. font-weight: 600;
  1354. color: #333;
  1355. margin: 0;
  1356. }
  1357. .panel-tabs {
  1358. display: flex;
  1359. align-items: center;
  1360. gap: 8px;
  1361. }
  1362. .sort-section {
  1363. display: flex;
  1364. align-items: center;
  1365. gap: 4px;
  1366. }
  1367. .divider {
  1368. color: #E4E7ED;
  1369. font-weight: normal;
  1370. }
  1371. .sort-btn {
  1372. padding: 0;
  1373. margin: 0;
  1374. width: 20px;
  1375. height: 20px;
  1376. display: flex;
  1377. align-items: center;
  1378. justify-content: center;
  1379. }
  1380. .sort-btn:hover {
  1381. background-color: #f5f7fa;
  1382. border-radius: 2px;
  1383. }
  1384. .chart-container {
  1385. min-height: 250px;
  1386. height: 280px;
  1387. position: relative;
  1388. box-sizing: border-box;
  1389. overflow: hidden;
  1390. display: flex;
  1391. align-items: center;
  1392. justify-content: center;
  1393. flex-shrink: 0;
  1394. }
  1395. .table-container {
  1396. flex: 1;
  1397. border: 1px solid #E4E7ED;
  1398. border-radius: 4px;
  1399. }
  1400. .table-container :deep(.el-table__body tr:nth-child(even)) {
  1401. background-color: #F8F8F8;
  1402. }
  1403. .table-container :deep(.el-table__header th),
  1404. .table-container :deep(.el-table__body td) {
  1405. font-weight: 500;
  1406. font-size: 13px;
  1407. color: #333333;
  1408. }
  1409. .echarts-chart {
  1410. width: 100% !important;
  1411. height: 100% !important;
  1412. min-height: 200px;
  1413. display: block;
  1414. }
  1415. /* 加载状态样式 */
  1416. .loading-container {
  1417. display: flex;
  1418. align-items: center;
  1419. justify-content: center;
  1420. gap: 8px;
  1421. padding: 20px;
  1422. background: white;
  1423. border-radius: 8px;
  1424. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1425. margin-bottom: 16px;
  1426. color: #409EFF;
  1427. font-size: 14px;
  1428. }
  1429. .loading-overlay {
  1430. position: relative;
  1431. }
  1432. .loading-overlay::after {
  1433. content: '';
  1434. position: absolute;
  1435. top: 0;
  1436. left: 0;
  1437. right: 0;
  1438. bottom: 0;
  1439. background: rgba(255, 255, 255, 0.7);
  1440. border-radius: 8px;
  1441. z-index: 10;
  1442. }
  1443. .loading-overlay::before {
  1444. content: '';
  1445. position: absolute;
  1446. top: 50%;
  1447. left: 50%;
  1448. transform: translate(-50%, -50%);
  1449. width: 40px;
  1450. height: 40px;
  1451. border: 3px solid #f3f3f3;
  1452. border-top: 3px solid #409EFF;
  1453. border-radius: 50%;
  1454. animation: spin 1s linear infinite;
  1455. z-index: 11;
  1456. }
  1457. @keyframes spin {
  1458. 0% {
  1459. transform: translate(-50%, -50%) rotate(0deg);
  1460. }
  1461. 100% {
  1462. transform: translate(-50%, -50%) rotate(360deg);
  1463. }
  1464. }
  1465. /* 响应式设计 */
  1466. @media (max-width: 1200px) {
  1467. .two-panel-layout {
  1468. flex-direction: column;
  1469. }
  1470. }
  1471. @media (max-width: 768px) {
  1472. .performance-analysis {
  1473. padding: 16px;
  1474. }
  1475. .header {
  1476. flex-direction: column;
  1477. gap: 16px;
  1478. align-items: flex-start;
  1479. }
  1480. .header-right {
  1481. align-self: flex-end;
  1482. }
  1483. .form-row {
  1484. flex-direction: column;
  1485. align-items: flex-start;
  1486. }
  1487. .form-content {
  1488. flex-direction: column;
  1489. align-items: flex-start;
  1490. }
  1491. }
  1492. </style>