StatisticsPersonnel.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <template>
  2. <div class="p-small radius-base fill-blank statistics-personnel">
  3. <base-table
  4. ref="tableRef"
  5. border
  6. stripe
  7. size="small"
  8. :data="sortTableData"
  9. :columns="columns"
  10. :height="tableHeight"
  11. :row-class-name="rowClassName"
  12. highlight-current-row
  13. @current-change="onCurrentChange"
  14. @row-dblclick="myDbClick"
  15. @row-contextmenu="rowContextmenu"
  16. @sort-change="sortChange"
  17. >
  18. <template #column-marker="{ row }">
  19. <!-- <el-popover
  20. v-if="row.markerId"
  21. :ref="(popover: any) => setPopoverRefs(row.markerId,popover)"
  22. trigger="contextmenu"
  23. placement="right"
  24. :show-arrow="false"
  25. transition="none"
  26. @show="onPopoverShow(row.markerId)"
  27. >
  28. <template #reference>
  29. <el-button size="small" link type="primary">{{ row.markerName }}</el-button>
  30. </template>
  31. <el-menu @select="() => {}">
  32. <el-menu-item index="setting" @click="onSetWorkload(row)">
  33. <el-button link>设置工作量</el-button>
  34. </el-menu-item>
  35. <el-menu-item index="send" @click="onSendMessage(row)">
  36. <el-button link>发送消息</el-button>
  37. </el-menu-item>
  38. </el-menu>
  39. </el-popover> -->
  40. <span v-if="row.markerId">{{ row.markerName }}</span>
  41. <span v-else-if="row.markingGroupNumber == 0">全体</span>
  42. <span v-else>全组</span>
  43. </template>
  44. </base-table>
  45. </div>
  46. <!-- <div
  47. v-if="!!current && !!current?.markerId"
  48. v-loading="loading1 || loading2"
  49. class="flex justify-between m-t-base charts-box"
  50. >
  51. <el-button type="primary" plain size="small" class="close-panel" @click="clearCheck">关闭</el-button>
  52. <div class="flex-1 p-base radius-base fill-blank m-r-base chart-box">
  53. <vue-e-charts class="full" :option="markerSubjectiveChartsOption"></vue-e-charts>
  54. </div>
  55. <div class="flex-1 p-base radius-base fill-blank chart-box">
  56. <vue-e-charts class="full" :option="markerObjectiveChartsOption"></vue-e-charts>
  57. </div>
  58. </div> -->
  59. <set-workload v-model="setWorkloadVisible" :data="setWorkloadData" />
  60. <right-key-menu
  61. v-show="visable"
  62. ref="rightKeyMenu"
  63. @right-click="rightClick"
  64. @on-set-workload="onSetWorkload"
  65. @on-send-message="onSendMessage"
  66. @force-assessment="forceAssessment"
  67. ></right-key-menu>
  68. </template>
  69. <script setup lang="tsx" name="StatisticsPersonnel">
  70. /** 人员数据统计-按人员展开 */
  71. import { ref, inject, computed, watch, nextTick, unref } from 'vue'
  72. import { useRouter } from 'vue-router'
  73. import { ElButton, ElPopover, ElMenu, ElMenuItem } from 'element-plus'
  74. import VueECharts from 'vue-echarts'
  75. import BaseTable from '@/components/element/BaseTable.vue'
  76. import SetWorkload from './SetWorkload.vue'
  77. import useVW, { usePX } from '@/hooks/useVW'
  78. import useFetch from '@/hooks/useFetch'
  79. import useTableCheck from '@/hooks/useTableCheck'
  80. import RightKeyMenu from './RightKeyMenu.vue'
  81. import useMainStore from '@/store/main'
  82. import type { EChartsOption } from 'echarts'
  83. import type { ExtractApiResponse, ExtractApiParams } from '@/api/api'
  84. import type { EpTableColumn } from 'global-type'
  85. import type { PopoverInstance } from 'element-plus'
  86. const mainStore = useMainStore()
  87. const isChief = computed(() => {
  88. const arr = ['CHIEF', 'SECTION_LEADER', 'EXPERT']
  89. return arr.indexOf(mainStore.myUserInfo?.role || '') > -1
  90. })
  91. const rightKeyMenu = ref(null)
  92. const rightClick = () => {
  93. visable.value = false
  94. }
  95. const props = defineProps<{
  96. data: ExtractApiResponse<'getStatisticsByGroup'>
  97. params: ExtractApiParams<'getStatisticsByGroup'> & { expand: boolean }
  98. filterColumns?: any
  99. result?: any
  100. }>()
  101. const rowClassName = (obj: any) => {
  102. if (obj.row.markingGroupNumber === 0) {
  103. return 'fixed-row'
  104. } else if (obj.row.isGroupTotalRow) {
  105. return 'fixed-row2'
  106. }
  107. }
  108. const setMessageVisible = inject<(visible: boolean) => void>('setMessageVisible')
  109. const setReplyUserId = inject<(id: number) => void>('setReplyUserId')
  110. const { push } = useRouter()
  111. const columns = computed(() => {
  112. return [
  113. {
  114. label: '#',
  115. type: 'index',
  116. width: 50,
  117. align: 'center',
  118. fixed: 'left',
  119. index(index: number) {
  120. return index + 1
  121. },
  122. },
  123. {
  124. label: '小组',
  125. prop: 'markingGroupNumber',
  126. width: 60,
  127. fixed: 'left',
  128. formatter(row: any) {
  129. return row.markingGroupNumber === 0 ? '全部' : `第${row.markingGroupNumber}组`
  130. },
  131. },
  132. { label: '评卷员', prop: 'markerName', minWidth: 84, slotName: 'marker', fixed: 'left' },
  133. { align: 'center', label: '评卷份数', prop: 'markingPaperCount', minWidth: 96 },
  134. { align: 'center', label: '平均分', prop: 'avg', minWidth: 80 },
  135. {
  136. align: 'center',
  137. label: '相关系数',
  138. prop: 'xyRelate',
  139. minWidth: 96,
  140. formatter(row: any) {
  141. let r = unref(props.result) || []
  142. const { markingGroupNumber } = row
  143. let compareTarget = r.find((item: any) => item.markingGroupNumber == markingGroupNumber)
  144. let allTarget = r.find((item: any) => item.markingGroupNumber == 0)
  145. if (compareTarget && markingGroupNumber) {
  146. return (
  147. props.params.markingGroupNumber ? row.xyRelate < compareTarget.xyRelate : row.xyRelate < allTarget.xyRelate
  148. ) ? (
  149. <span style="color:red">{row.xyRelate}</span>
  150. ) : (
  151. row.xyRelate
  152. )
  153. } else {
  154. return row.xyRelate
  155. }
  156. },
  157. },
  158. { align: 'center', label: '标准差', prop: 'std', minWidth: 80 },
  159. { align: 'center', label: '综合系数', prop: 'integration', minWidth: 96 },
  160. // { align: 'center', label: '评卷份数', prop: 'markingPaperCount', width: 92 },
  161. { align: 'center', label: '当日可阅', prop: 'markDayCount', minWidth: 96 },
  162. { align: 'center', label: '剩余可阅', prop: 'todoMarkDayCount', minWidth: 96 },
  163. { align: 'center', label: '客观题0分量', prop: 'objectiveZero', minWidth: 120 },
  164. { align: 'center', label: '客观平均分', prop: 'objectiveAvg', minWidth: 110 },
  165. { align: 'center', label: '客观标准差', prop: 'objectiveStd', minWidth: 110 },
  166. {
  167. align: 'center',
  168. label: '重评/待确认',
  169. prop: 'reMarkUnConfirmCount',
  170. minWidth: 120,
  171. formatter(row: any) {
  172. return `${row.reMarkCount}/${row.reMarkUnConfirmCount}`
  173. },
  174. },
  175. { align: 'center', label: '抽查量', prop: 'checkCount', minWidth: 90 },
  176. { align: 'center', label: '抽查改正量', prop: 'checkCorrectCount', minWidth: 110 },
  177. // { align: 'center', label: '相关系数', prop: 'xyRelate', width: 90 },
  178. // { align: 'center', label: '平均分', prop: 'avg', width: 80 },
  179. // { align: 'center', label: '标准差', prop: 'std', width: 80 },
  180. { align: 'center', label: '近5分钟最高分', prop: 'scoreTop', minWidth: 130 },
  181. { align: 'center', label: '近5分钟最低分', prop: 'scoreLow', minWidth: 130 },
  182. { align: 'center', label: '近5分钟客主比', prop: 'objSubRate', minWidth: 130 },
  183. { align: 'center', label: '平均客主比', prop: 'objSubAvgRate', minWidth: 110 },
  184. {
  185. align: 'center',
  186. label: '在线',
  187. prop: 'online',
  188. minWidth: 72,
  189. formatter(row: any) {
  190. if (row.markingGroupNumber === 0) {
  191. let total = (unref(props.result) || [])
  192. .filter((item: any) => item.markingGroupNumber !== 0)
  193. .reduce((ar: any, item: any) => {
  194. return [...ar, ...(item.markerDetails || [])]
  195. }, [])
  196. .reduce((num: number, item: any) => {
  197. return num + (item.online ? 1 : 0)
  198. }, 0)
  199. return total + '人'
  200. } else if (row.isGroupTotalRow) {
  201. let total = (unref(props.result) || [])
  202. .filter((item: any) => item.markingGroupNumber == row.markingGroupNumber)
  203. .reduce((ar: any, item: any) => {
  204. return [...ar, ...(item.markerDetails || [])]
  205. }, [])
  206. .reduce((num: number, item: any) => {
  207. return num + (item.online ? 1 : 0)
  208. }, 0)
  209. return total + '人'
  210. } else {
  211. return row.online ? '在线' : '离线'
  212. }
  213. },
  214. },
  215. { align: 'center', label: '状态', prop: 'markingStatus', minWidth: 100 },
  216. { align: 'center', label: '速度', prop: 'markingRate', minWidth: 72 },
  217. // { align: 'center', label: '综合系数', prop: 'integration', width: 90 },
  218. ]
  219. .map((col: any) => {
  220. if (!['小组', '重评/待确认'].includes(col.label)) col.sortable = 'custom'
  221. return col
  222. })
  223. .filter((col: any) => {
  224. return props.filterColumns ? props.filterColumns.indexOf(col.prop) > -1 || col.type === 'index' : col
  225. })
  226. })
  227. const visable = ref(false)
  228. const rowContextmenu = (row: any, column: any, event: any) => {
  229. console.log(row, column, event)
  230. visable.value = false
  231. setTimeout(() => {
  232. visable.value = true
  233. }, 50)
  234. event.preventDefault()
  235. nextTick(() => {
  236. // alert(1)
  237. ;(rightKeyMenu.value as any).onload(row, column, event)
  238. })
  239. }
  240. const popovers = ref<Record<string, PopoverInstance>>({})
  241. const setWorkloadVisible = ref<boolean>(false)
  242. const setWorkloadData = ref<ExtractArrayValue<ExtractApiResponse<'getStatisticsByGroup'>>>()
  243. function setPopoverRefs(id: number, popover: PopoverInstance) {
  244. popovers.value[`popovers-${id}`] = popover
  245. }
  246. /** on popover show */
  247. function onPopoverShow(id: number) {
  248. Object.keys(popovers.value).forEach((k) => {
  249. if (k !== `popovers-${id}`) {
  250. popovers.value[k]?.hide()
  251. }
  252. })
  253. }
  254. /** 设置工作量 */
  255. function onSetWorkload(data: ExtractArrayValue<ExtractApiResponse<'getStatisticsByGroup'>>) {
  256. popovers.value[`popovers-${data.markerId}`]?.hide()
  257. setWorkloadData.value = data
  258. setWorkloadVisible.value = true
  259. }
  260. /** 发送消息 */
  261. function onSendMessage(data: ExtractArrayValue<ExtractApiResponse<'getStatisticsByGroup'>>) {
  262. popovers.value[`popovers-${data.markerId}`]?.hide()
  263. setReplyUserId?.(data.markerId)
  264. setMessageVisible?.(true)
  265. }
  266. function forceAssessment(data: ExtractArrayValue<ExtractApiResponse<'getStatisticsByGroup'>>) {
  267. push({
  268. name: 'MarkingAssess',
  269. query: { markerId: data.markerId },
  270. })
  271. }
  272. const data = computed(() => {
  273. return JSON.parse(JSON.stringify(props.data || [])) || []
  274. })
  275. const { tableRef, tableData, current, onCurrentChange, onDbClick, currentView, elTableRef } = useTableCheck(data, false)
  276. const myDbClick = (row: any) => {
  277. onDbClick(row)
  278. if (currentView.value && !!currentView.value.markingGroupNumber && !!currentView.value.markerId) {
  279. push({
  280. name: 'AnalysisPersonnelStatisticsMarker',
  281. query: {
  282. markerId: currentView.value.markerId,
  283. markerName: currentView.value.markerName,
  284. source: '人员数据统计',
  285. subjectCode: props.params?.subjectCode,
  286. questionMainNumber: props.params?.questionMainNumber,
  287. },
  288. })
  289. }
  290. }
  291. const sortTableData = ref<any[]>([])
  292. const originalTableData = ref<any[]>([])
  293. const sortChange = (params: any) => {
  294. const { column, prop, order } = params
  295. if (order === 'ascending') {
  296. sortTableData.value.sort((a: any, b: any) => {
  297. if (a.markingGroupNumber == 0) {
  298. return -1
  299. } else if (b.markingGroupNumber == 0) {
  300. return 1
  301. } else {
  302. if (typeof a[prop] === 'string') {
  303. let aa = a[prop] || '',
  304. bb = b[prop] || ''
  305. return aa.localeCompare(bb)
  306. } else {
  307. return a[prop] - b[prop]
  308. }
  309. }
  310. })
  311. } else if (order === 'descending') {
  312. sortTableData.value.sort((a: any, b: any) => {
  313. if (a.markingGroupNumber == 0) {
  314. return -1
  315. } else if (b.markingGroupNumber == 0) {
  316. return 1
  317. } else {
  318. if (typeof a[prop] === 'string') {
  319. let aa = a[prop] || '',
  320. bb = b[prop] || ''
  321. return bb.localeCompare(aa)
  322. } else {
  323. return b[prop] - a[prop]
  324. }
  325. }
  326. })
  327. } else if (order == null) {
  328. sortTableData.value = JSON.parse(JSON.stringify(originalTableData.value))
  329. }
  330. }
  331. function initSortTableData() {
  332. if (tableData.value) {
  333. let data: any[] = JSON.parse(JSON.stringify(tableData.value)) || []
  334. if (data.length && data[data.length - 1].markingGroupNumber === 0) {
  335. let last = data.splice(data.length - 1, 1)[0]
  336. data.unshift(last)
  337. }
  338. sortTableData.value = JSON.parse(JSON.stringify(data))
  339. originalTableData.value = JSON.parse(JSON.stringify(data))
  340. }
  341. }
  342. initSortTableData()
  343. watch(tableData, () => {
  344. popovers.value = {}
  345. initSortTableData()
  346. })
  347. // watch(currentView, () => {
  348. // if (currentView.value && !!currentView.value.markingGroupNumber) {
  349. // push({
  350. // name: 'AnalysisPersonnelStatisticsMarker',
  351. // query: { markerId: currentView.value.markerId, markerName: currentView.value.markerName },
  352. // })
  353. // }
  354. // })
  355. const {
  356. fetch: getStatisticObjectiveByMarker,
  357. result: objectiveByMarker,
  358. loading: loading1,
  359. } = useFetch('getStatisticObjectiveByMarker')
  360. const {
  361. fetch: getStatisticSubjectiveByMarker,
  362. result: subjectiveByMarker,
  363. loading: loading2,
  364. } = useFetch('getStatisticSubjectiveByMarker')
  365. watch(
  366. [() => props.params, current],
  367. () => {
  368. const { startTime = '', endTime = '' } = props.params || {}
  369. // if (current.value?.markerId) {
  370. if (current.value?.markerId) {
  371. getStatisticObjectiveByMarker({
  372. markerId: current.value.markerId,
  373. startTime,
  374. endTime,
  375. })
  376. getStatisticSubjectiveByMarker({
  377. markerId: current.value.markerId,
  378. startTime,
  379. endTime,
  380. })
  381. }
  382. },
  383. { immediate: true, deep: true }
  384. )
  385. type StatisticObjectiveByMarker = ExtractApiResponse<'getStatisticObjectiveByMarker'>
  386. type StatisticObjectiveByMarkerValues = StatisticObjectiveByMarker['segmentsByAll']
  387. const getXAxisData = <K extends keyof ExtractArrayValue<StatisticObjectiveByMarkerValues>>(
  388. field: K,
  389. data?: StatisticObjectiveByMarkerValues
  390. ) => {
  391. if (!data) {
  392. return []
  393. }
  394. const getValue = (key: K, item: ExtractArrayValue<StatisticObjectiveByMarkerValues>) => {
  395. return item[key]
  396. }
  397. return data?.map((v) => getValue(field, v))
  398. }
  399. const markerSubjectiveChartsOption = computed<EChartsOption>(() => {
  400. return {
  401. legend: {
  402. right: 0,
  403. itemWidth: 14,
  404. data: ['评卷员主观分布', '小组主观分布', '题组主观分布'],
  405. },
  406. xAxis: {
  407. axisLine: { show: false },
  408. axisTick: { show: false },
  409. splitLine: { show: false },
  410. axisLabel: {
  411. align: 'right',
  412. },
  413. data: getXAxisData('scoreStart', subjectiveByMarker?.value?.segmentsByAll),
  414. },
  415. yAxis: [
  416. {
  417. type: 'value',
  418. },
  419. {
  420. type: 'value',
  421. axisLabel: {
  422. formatter: `{value}%`,
  423. },
  424. splitLine: { show: false },
  425. },
  426. ],
  427. series: [
  428. {
  429. name: '评卷员主观分布',
  430. type: 'line',
  431. itemStyle: {
  432. color: '#3AD500',
  433. },
  434. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByUser),
  435. },
  436. {
  437. name: '小组主观分布',
  438. type: 'line',
  439. itemStyle: {
  440. color: '#0064FF',
  441. },
  442. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByGroup),
  443. },
  444. {
  445. name: '题组主观分布',
  446. type: 'line',
  447. itemStyle: {
  448. color: '#008000',
  449. },
  450. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByAll),
  451. },
  452. ],
  453. }
  454. })
  455. const markerObjectiveChartsOption = computed<EChartsOption>(() => {
  456. return {
  457. legend: {
  458. right: 0,
  459. itemWidth: 14,
  460. data: ['评卷员客观分布', '小组客观分布', '题组客观分布'],
  461. },
  462. xAxis: {
  463. axisLine: { show: false },
  464. axisTick: { show: false },
  465. splitLine: { show: false },
  466. axisLabel: {
  467. align: 'right',
  468. },
  469. data: getXAxisData('scoreStart', objectiveByMarker?.value?.segmentsByAll),
  470. },
  471. yAxis: [
  472. {
  473. type: 'value',
  474. },
  475. {
  476. type: 'value',
  477. axisLabel: {
  478. formatter: `{value}%`,
  479. },
  480. splitLine: { show: false },
  481. },
  482. ],
  483. series: [
  484. {
  485. name: '评卷员客观分布',
  486. type: 'line',
  487. itemStyle: {
  488. color: '#3AD500',
  489. },
  490. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByUser),
  491. },
  492. {
  493. name: '小组客观分布',
  494. type: 'line',
  495. itemStyle: {
  496. color: '#0064FF',
  497. },
  498. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByGroup),
  499. },
  500. {
  501. name: '题组客观分布',
  502. type: 'line',
  503. itemStyle: {
  504. color: '#008000',
  505. },
  506. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByAll),
  507. },
  508. ],
  509. }
  510. })
  511. const tableHeight = computed(() => {
  512. // return !!current.value && !!current.value?.markerId ? 'calc(100vh - 520px)' : 'calc(100vh - 219px)'
  513. return 'calc(100vh - 250px)'
  514. })
  515. const clearCheck = () => {
  516. ;(elTableRef as any).value!.setCurrentRow(undefined)
  517. }
  518. </script>
  519. <style scoped lang="scss">
  520. .statistics-personnel {
  521. :deep(.el-table) {
  522. .fixed-row {
  523. display: table-row;
  524. position: sticky;
  525. position: '-webkit-sticky';
  526. top: 0;
  527. width: 100%;
  528. z-index: 3;
  529. font-weight: bold;
  530. color: #333;
  531. }
  532. .fixed-row2 {
  533. display: table-row;
  534. position: sticky;
  535. position: '-webkit-sticky';
  536. top: 44px;
  537. width: 100%;
  538. z-index: 3;
  539. font-weight: bold;
  540. color: #333;
  541. }
  542. }
  543. }
  544. .chart-box {
  545. height: 293px;
  546. }
  547. .charts-box {
  548. position: relative;
  549. .close-panel {
  550. position: absolute;
  551. top: 50%;
  552. left: 50%;
  553. transform: translateX(-50%) translateY(-50%);
  554. z-index: 1;
  555. }
  556. }
  557. </style>