StandardExecution.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <template>
  2. <ChartsContainer title="标准执行">
  3. <div style="height: 100%; flex-direction: column; display: flex;">
  4. <div class="cards-container">
  5. <div class="card-item" v-for=" item in cardData " :key="item.label">
  6. <div>{{ item.label }}</div>
  7. <div :style="'--color:' + item.color">{{ item.value }}</div>
  8. </div>
  9. </div>
  10. <div class="charts-container">
  11. <div class="charts-container-item">
  12. <div class="charts-container-name">问题分布项</div>
  13. <div class="charts-container-content" ref="problem" v-show="radarData.grounp.length > 1" />
  14. <div class="charts-container-content" v-show="radarData.grounp.length < 1" style="margin-top: 80px; text-align: center; color: #fff">
  15. {{ radarData.grounp.length < 1 ? '暂无数据' : '' }}
  16. </div>
  17. </div>
  18. <div class="charts-container-item">
  19. <div class="charts-container-name">测试总错题数</div>
  20. <div class="charts-container-content" ref="wrongAnswer"/>
  21. </div>
  22. </div>
  23. </div>
  24. </ChartsContainer>
  25. </template>
  26. <script setup>
  27. import ChartsContainer from '../ChartsContainer.vue';
  28. import { computed, ref, reactive } from 'vue';
  29. import { useTimeOut } from './useTimeOut';
  30. import { getSiteProfile, getDeptProfile, getUserProfile, getPortrait } from '@/api/item/items'
  31. import { useECharts } from '@/hooks/useEcharts.js';
  32. const props = defineProps({
  33. type: {
  34. type: String,
  35. default: '' // team, department, station
  36. }
  37. })
  38. const problem = ref(null)
  39. const wrongAnswer = ref(null)
  40. const portraitData = ref({
  41. })
  42. const profileData = ref({
  43. rankingStats: {}
  44. })
  45. const params = inject('provideParams')
  46. const cardData = computed(() => {
  47. return [
  48. props.type ? {
  49. label: '巡检排名',
  50. value: `${portraitData.value.stationRanking || 0} / ${portraitData.value.stationTotal || 0}`,
  51. color: '#35D8FF'
  52. } : undefined,
  53. { label: '巡检总问题数', value: portraitData.value.sumCount || 0, color: '#35D8FF' },
  54. { label: '按期整改', value: portraitData.value.onTimeCompletedCount || 0, color: '#FFC061' },
  55. { label: '超期整改', value: portraitData.value.overTimeCompletedCount || 0, color: '#DD4D43' },
  56. props.type ? {
  57. label: '测试排名',
  58. value: `${profileData.value.rankingStats.siteRanking || 0} / ${profileData.value.rankingStats.siteTotalDepts || profileData.value.rankingStats.siteTotalUsers || 0 }`,
  59. color: '#35D8FF'
  60. } : undefined,
  61. !props.type ? {
  62. label: '测试平均分',
  63. value: profileData.value.scoreStats && profileData.value.scoreStats.totalAvgScore
  64. || profileData.value.avgScore || 0, color: '#35D8FF'
  65. } : undefined,
  66. !props.type ? { label: '测试总错题数', value: profileData.value.totalErrors || 0, color: '#35D8FF' } : undefined,
  67. ].filter(Boolean)
  68. })
  69. const radarData = reactive({
  70. legend: [],
  71. grounp: [],
  72. data: []
  73. })
  74. const problemOption = computed(() => {
  75. return {
  76. legend: props.type ? {
  77. show: false
  78. } : {
  79. data: radarData.legend,
  80. top: 10,
  81. textStyle: {
  82. color: '#fff',
  83. fontSize: 12
  84. }
  85. },
  86. radar: {
  87. indicator: radarData.grounp,
  88. shape: 'polygon',
  89. splitNumber: 4,
  90. radius: '50%',
  91. center: [ '50%', radarData.legend.length ? '60%' : '50%' ], // 缩小雷达图并居中,为label留出空间
  92. axisName: {
  93. color: '#fff',
  94. fontSize: 12
  95. },
  96. },
  97. series: [ {
  98. type: 'radar',
  99. data: radarData.data,
  100. emphasis: {
  101. lineStyle: {
  102. width: 4
  103. }
  104. }
  105. } ],
  106. tooltip: {
  107. trigger: 'item'
  108. }
  109. };
  110. })
  111. useECharts(problem, problemOption)
  112. const wrongAnswerData = reactive({
  113. legend: [],
  114. grounp: [
  115. { text: '操作执行' },
  116. { text: '传达学习' },
  117. { text: '班组管理' },
  118. { text: '工作纪律' },
  119. { text: '设施设备' },
  120. { text: '通道样貌' }
  121. ],
  122. data: []
  123. })
  124. const wrongAnswerOption = computed(() => {
  125. return {
  126. legend: {
  127. data: wrongAnswerData.legend,
  128. top: 15,
  129. textStyle: {
  130. color: '#fff',
  131. fontSize: 12
  132. }
  133. },
  134. radar: {
  135. indicator: wrongAnswerData.grounp,
  136. shape: 'polygon',
  137. splitNumber: 4,
  138. radius: '50%',
  139. center: [ '50%', wrongAnswerData.legend.length ? '60%' : '50%' ], // 缩小雷达图并居中,为label留出空间
  140. axisName: {
  141. color: '#fff',
  142. fontSize: 12
  143. },
  144. },
  145. series: [ {
  146. type: 'radar',
  147. data: wrongAnswerData.data,
  148. emphasis: {
  149. lineStyle: {
  150. width: 4
  151. }
  152. }
  153. } ],
  154. tooltip: {
  155. trigger: 'item'
  156. }
  157. };
  158. })
  159. useECharts(wrongAnswer, wrongAnswerOption)
  160. const color = ['#408CFF', '#58A55C', '#DD4D43', '#FAC858']
  161. const problemRadarDataHandler = (result = []) => {
  162. const grounpObj = result.reduce((cur, acc) => {
  163. if (cur[ acc.deptName ]) {
  164. cur[ acc.deptName ].push(acc)
  165. } else {
  166. cur[ acc.deptName ] = [ acc ]
  167. }
  168. return cur
  169. }, {})
  170. const grounp = Object.entries(grounpObj)
  171. radarData.legend =grounp.length > 1 ? grounp.map(item => {
  172. return item[ 0 ]
  173. }) : []
  174. radarData.grounp = grounp[ 0 ] && grounp[ 0 ][ 1 ] ? grounp[ 0 ][ 1 ].map(item => ({ text: item.name })) : []
  175. radarData.data = grounp.map((item, index) => {
  176. return {
  177. name: props.type === 'team' ? '问题分布项' : item[ 0 ],
  178. value: item[ 1 ].map(attr => attr.total),
  179. // // 为每个数据系列设置不同的颜色
  180. color: color[ index % color.length ],
  181. areaStyle: {
  182. opacity: 0.5,
  183. color: color[ index % color.length ],
  184. },
  185. lineStyle: {
  186. width: 2
  187. },
  188. itemStyle: {
  189. borderWidth: 2,
  190. color: color[ index % color.length ],
  191. }
  192. }
  193. })
  194. }
  195. useTimeOut(() => {
  196. if (!props.type) {
  197. params.value.deptId && getSiteProfile({ siteId: params.value.deptId }).then(res => {
  198. profileData.value = res.data
  199. const { deptCategoryErrors = [] } = res.data
  200. wrongAnswerData.legend = deptCategoryErrors.map(item => item.deptName)
  201. wrongAnswerData.data = deptCategoryErrors.map((item, index) => {
  202. return {
  203. name: item.deptName,
  204. value: wrongAnswerData.grounp.map(attr => {
  205. const rowData = item.categoryErrors.find(row => row.categoryName === attr.text) || { errorCount: 0 }
  206. return rowData.errorCount
  207. }),
  208. areaStyle: {
  209. opacity: 0.5,
  210. color: color[ index % color.length ],
  211. },
  212. lineStyle: {
  213. width: 2
  214. },
  215. itemStyle: {
  216. borderWidth: 2,
  217. color: color[ index % color.length ],
  218. }
  219. }
  220. })
  221. })
  222. } else {
  223. const invokerApi = props.type === 'department' ?
  224. params.value.deptId && getDeptProfile({ deptId: params.value.deptId || '' }) :
  225. props.type === 'personal' ? getUserProfile({ userId: params.value.deptId || '' }) : getUserProfile({ deptId: params.value.deptId || '' })
  226. params.value.deptId && invokerApi.then(res => {
  227. profileData.value = res.data
  228. const deptCategoryErrors = [res.data]
  229. wrongAnswerData.legend = []
  230. wrongAnswerData.data = deptCategoryErrors.map((item, index) => {
  231. return {
  232. name: item.deptName,
  233. value: wrongAnswerData.grounp.map((attr, index) => {
  234. const rowData = item.categoryErrors.find(row => row.categoryName === attr.text) || { errorCount: 0 }
  235. return rowData.errorCount
  236. }),
  237. color: color[ index % color.length ],
  238. areaStyle: {
  239. opacity: 0.5,
  240. color: color[ index % color.length ],
  241. },
  242. lineStyle: {
  243. width: 2
  244. },
  245. itemStyle: {
  246. borderWidth: 2,
  247. color: color[ index % color.length ],
  248. }
  249. }
  250. })
  251. })
  252. }
  253. const portraitParams = {}
  254. if (props.type === 'department') {
  255. portraitParams.checkedDepartmentId = params.value.deptId
  256. } else if (props.type === 'team') {
  257. portraitParams.checkedTeamId = params.value.deptId
  258. } else if (props.type === 'personal') {
  259. portraitParams.checkedUserId = params.value.deptId
  260. } else {
  261. portraitParams.checkedSiteId = params.value.deptId
  262. }
  263. params.value.deptId && getPortrait(portraitParams).then(res => {
  264. portraitData.value = res.data || {}
  265. const { checkLargeScreenCommonDtoList = [] } = res.data || {}
  266. problemRadarDataHandler(checkLargeScreenCommonDtoList.filter(item => item && item.deptName !== '总数'))
  267. })
  268. }, [ params ])
  269. </script>
  270. <style lang="scss" scoped>
  271. .cards-container {
  272. display: flex;
  273. align-items: center;
  274. column-gap: 15px;
  275. padding-bottom: 15px;
  276. .card-item {
  277. flex: 1;
  278. color: #fff;
  279. font-weight: bold;
  280. font-size: 18px;
  281. text-align: center;
  282. background: url('../../../../../assets/images/cardBg.png') no-repeat;
  283. background-size: 100% 100%;
  284. display: flex;
  285. flex-direction: column;
  286. align-items: center;
  287. justify-content: center;
  288. row-gap: 10px;
  289. border-radius: 5px;
  290. font-weight: bold;
  291. padding: 10px 5px;
  292. div:nth-child(1) {
  293. white-space: nowrap;
  294. text-overflow: ellipsis;
  295. overflow: hidden;
  296. min-width: 0;
  297. }
  298. div:nth-child(2) {
  299. font-size: 20px;
  300. color: var(--color)
  301. }
  302. }
  303. }
  304. .charts-container {
  305. display: flex;
  306. flex: 1;
  307. .charts-container-item {
  308. flex: 1;
  309. height: 100%;
  310. display: flex;
  311. flex-direction: column;
  312. }
  313. .charts-container-name {
  314. color: #fff;
  315. font-weight: bold;
  316. font-size: 16px;
  317. display: flex;
  318. align-items: center;
  319. &::before {
  320. content: '';
  321. display: inline-block;
  322. border-left: 9px solid #1CB6FF;
  323. border-top: 8px solid transparent;
  324. border-right: 9px solid transparent;
  325. border-bottom: 8px solid transparent;
  326. }
  327. }
  328. .charts-container-content {
  329. flex: 1;
  330. width: 100%;
  331. }
  332. }
  333. </style>