index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <template>
  2. <div class="flex direction-column full">
  3. <div class="flex items-center p-extra-small fill-blank header-view">
  4. <el-button class="m-r-auto" size="small" plain @click="back()">返回</el-button>
  5. <div class="m-r-base m-l-base">
  6. <lock-entry></lock-entry>
  7. </div>
  8. <message class="m-r-base m-l-base" :paper-path="current?.filePath"></message>
  9. <div class="m-r-base m-l-base">
  10. <user-info></user-info>
  11. </div>
  12. </div>
  13. <div class="flex fill-blank detail-info">
  14. <div class="flex p-base detail-info-chart" style="width: 64%">
  15. <div style="width: 50%">
  16. <vue-echarts class="full" :option="markerSubjectiveChartsOption"></vue-echarts>
  17. </div>
  18. <div class="flex-1 m-l-base">
  19. <vue-echarts class="full" :option="markerObjectiveChartsOption"></vue-echarts>
  20. </div>
  21. </div>
  22. <div class="flex direction-column p-extra-small" style="width: 36%">
  23. <div class="flex items-center justify-between detail-info-table-header m-b-mini">
  24. <el-button custom-1 size="small" class="detail-info-label">
  25. <span class="">{{ dataType }}试卷总数:</span>
  26. <span class="m-l-extra-small detail-info-label-num">{{ total }}</span>
  27. </el-button>
  28. <el-pagination
  29. v-bind="pagination"
  30. v-model:current-page="currentPage"
  31. size="small"
  32. class="m-t-unset"
  33. background
  34. right
  35. hide-on-single-page
  36. :pager-count="3"
  37. small
  38. ></el-pagination>
  39. </div>
  40. <div class="flex-1 scroll-auto">
  41. <base-table
  42. ref="tableRef"
  43. border
  44. stripe
  45. size="small"
  46. height="100%"
  47. :data="tableData"
  48. :columns="tableColumn"
  49. highlight-current-row
  50. @current-change="onCurrentChange"
  51. >
  52. <template #empty> 暂无数据 </template>
  53. </base-table>
  54. </div>
  55. </div>
  56. </div>
  57. <div class="flex-1 p-t-base flex overflow-hidden">
  58. <div class="flex direction-column flex-1 overflow-hidden radius-base fill-blank relative paper-view">
  59. <div class="flex-1 p-extra-small scroll-auto img-wrap">
  60. <img :src="current?.filePath" alt="" class="paper-img relative" />
  61. <p v-if="current" class="absolute mark-score">{{ current.headerScore ?? current.markerScore }}</p>
  62. </div>
  63. <span class="preview" @click="onPreview">
  64. <svg-icon name="preview"></svg-icon>
  65. </span>
  66. <scoring-panel-with-confirm
  67. :id="current?.taskId"
  68. v-model:visible="scoringPanelVisible"
  69. v-model:score="modelScore"
  70. :main-number="current?.mainNumber"
  71. @submit="onSubmit"
  72. ></scoring-panel-with-confirm>
  73. </div>
  74. <div class="radius-base p-extra-small fill-blank m-l-base overflow-auto paper-mark-record">
  75. <mark-history-list
  76. :id="current?.taskId"
  77. height="100%"
  78. :model-value="true"
  79. :modal="false"
  80. :task="current"
  81. ></mark-history-list>
  82. </div>
  83. </div>
  84. </div>
  85. <image-preview v-model="previewModalVisible" :url="current?.filePath"></image-preview>
  86. </template>
  87. <script setup lang="ts" name="AnalysisGroupDetail">
  88. /** 小组监控数据详情 */
  89. import { ref, computed, watch } from 'vue'
  90. import { useRoute, useRouter } from 'vue-router'
  91. import { ElButton, ElPagination } from 'element-plus'
  92. import useFetch from '@/hooks/useFetch'
  93. import useTable from '@/hooks/useTable'
  94. import useTableCheck from '@/hooks/useTableCheck'
  95. import VueEcharts from 'vue-echarts'
  96. import BaseTable from '@/components/element/BaseTable.vue'
  97. import Message from '@/components/shared/message/Message.vue'
  98. import UserInfo from '@/components/shared/UserInfo.vue'
  99. import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
  100. import ImagePreview from '@/components/shared/ImagePreview.vue'
  101. import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
  102. import SvgIcon from '@/components/common/SvgIcon.vue'
  103. import LockEntry from '@/components/common/LockEntry.vue'
  104. import type { EChartsOption } from 'echarts'
  105. import type { ExtractMultipleApiResponse, ExtractApiResponse } from '@/api/api'
  106. import type { EpTableColumn } from 'global-type'
  107. const { back } = useRouter()
  108. const { query } = useRoute()
  109. /** 给分板 */
  110. const scoringPanelVisible = ref<boolean>(true)
  111. /** 图片预览 */
  112. const previewModalVisible = ref<boolean>(false)
  113. /** 分数 */
  114. const modelScore = ref<number[]>([])
  115. /** 类型 */
  116. const dataType = computed(() => {
  117. return query.operateType === 'VIEW' ? '已浏览' : '已给分'
  118. })
  119. /** 预览试卷 */
  120. const onPreview = () => {
  121. previewModalVisible.value = true
  122. }
  123. /** 抽查详情列表 */
  124. const { pagination, currentPage, total, data, fetchTable } = useTable(
  125. 'getGroupMonitorDetail',
  126. {
  127. operateType: query.operateType as 'VIEW' | 'MARK',
  128. headerId: query.headerId as string,
  129. },
  130. { pageSize: 4 }
  131. )
  132. const { tableRef, current, onCurrentChange, tableData, next } = useTableCheck(data)
  133. /** 抽查详情表格配置 */
  134. const tableColumn: EpTableColumn<ExtractMultipleApiResponse<'getGroupMonitorDetail'>>[] = [
  135. { label: '密号', prop: 'secretNumber', width: 100, fixed: 'left' },
  136. { label: '评卷员', prop: 'markerName' },
  137. { label: '给分', prop: 'markerScore', width: 50 },
  138. { label: '组长给分', prop: 'headerScore', width: 70 },
  139. { label: '客观分', prop: 'objectiveScore', width: 60 },
  140. { label: '客主比', prop: 'headerRatio', width: 60 },
  141. { label: '分档', prop: 'scoreLevel' },
  142. { label: '评卷时间', prop: 'markTime', width: 130 },
  143. ]
  144. const { fetch: getStatisticObjectiveByMarker, result: objectiveByMarker } = useFetch('getStatisticObjectiveByMarker')
  145. const { fetch: getStatisticSubjectiveByMarker, result: subjectiveByMarker } = useFetch('getStatisticSubjectiveByMarker')
  146. watch(
  147. [current],
  148. () => {
  149. if (current.value?.markerId) {
  150. getStatisticObjectiveByMarker({
  151. markerId: current.value.markerId,
  152. startTime: '',
  153. endTime: '',
  154. })
  155. getStatisticSubjectiveByMarker({
  156. markerId: current.value.markerId,
  157. startTime: '',
  158. endTime: '',
  159. })
  160. }
  161. },
  162. { immediate: true, deep: true }
  163. )
  164. type StatisticObjectiveByMarker = ExtractApiResponse<'getStatisticObjectiveByMarker'>
  165. type StatisticObjectiveByMarkerValues = StatisticObjectiveByMarker['segmentsByAll']
  166. const getXAxisData = <K extends keyof ExtractArrayValue<StatisticObjectiveByMarkerValues>>(
  167. field: K,
  168. data?: StatisticObjectiveByMarkerValues
  169. ) => {
  170. if (!data) {
  171. return []
  172. }
  173. const getValue = (key: K, item: ExtractArrayValue<StatisticObjectiveByMarkerValues>) => {
  174. return item[key]
  175. }
  176. return data?.map((v) => getValue(field, v))
  177. }
  178. const markerSubjectiveChartsOption = computed<EChartsOption>(() => {
  179. return {
  180. title: { text: '主观分布' },
  181. grid: {
  182. bottom: 30,
  183. right: 0,
  184. },
  185. legend: {
  186. itemWidth: 14,
  187. data: ['评卷员', '小组', '题组'],
  188. right: 10,
  189. top: 2,
  190. },
  191. xAxis: {
  192. axisLine: { show: false },
  193. axisTick: { show: false },
  194. splitLine: { show: false },
  195. axisLabel: {
  196. align: 'right',
  197. },
  198. data: getXAxisData('scoreStart', subjectiveByMarker?.value?.segmentsByAll),
  199. },
  200. yAxis: [
  201. // {
  202. // type: 'value',
  203. // },
  204. {
  205. type: 'value',
  206. axisLabel: {
  207. formatter: `{value}%`,
  208. },
  209. splitLine: { show: true },
  210. },
  211. ],
  212. series: [
  213. {
  214. name: '评卷员',
  215. type: 'line',
  216. itemStyle: {
  217. color: '#3AD500',
  218. },
  219. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByUser),
  220. },
  221. {
  222. name: '小组',
  223. type: 'line',
  224. itemStyle: {
  225. color: '#0064FF',
  226. },
  227. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByGroup),
  228. },
  229. {
  230. name: '题组',
  231. type: 'line',
  232. itemStyle: {
  233. color: '#008000',
  234. },
  235. data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByAll),
  236. },
  237. ],
  238. }
  239. })
  240. const markerObjectiveChartsOption = computed<EChartsOption>(() => {
  241. return {
  242. title: { text: '客观分布' },
  243. grid: {
  244. bottom: 30,
  245. right: 0,
  246. },
  247. legend: {
  248. itemWidth: 14,
  249. data: ['评卷员', '小组', '题组'],
  250. right: 10,
  251. top: 2,
  252. },
  253. xAxis: {
  254. axisLine: { show: false },
  255. axisTick: { show: false },
  256. splitLine: { show: false },
  257. axisLabel: {
  258. align: 'right',
  259. },
  260. data: getXAxisData('scoreStart', objectiveByMarker?.value?.segmentsByAll),
  261. },
  262. yAxis: [
  263. // {
  264. // type: 'value',
  265. // },
  266. {
  267. type: 'value',
  268. axisLabel: {
  269. formatter: `{value}%`,
  270. },
  271. splitLine: { show: true },
  272. },
  273. ],
  274. series: [
  275. {
  276. name: '评卷员',
  277. type: 'line',
  278. itemStyle: {
  279. color: '#3AD500',
  280. },
  281. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByUser),
  282. },
  283. {
  284. name: '小组',
  285. type: 'line',
  286. itemStyle: {
  287. color: '#0064FF',
  288. },
  289. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByGroup),
  290. },
  291. {
  292. name: '题组',
  293. type: 'line',
  294. itemStyle: {
  295. color: '#008000',
  296. },
  297. data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByAll),
  298. },
  299. ],
  300. }
  301. })
  302. const onSubmit = async () => {
  303. try {
  304. if (!current.value?.taskId) {
  305. return
  306. }
  307. await useFetch('markMonitorDetailTask').fetch({ taskId: current.value.taskId, scores: modelScore.value })
  308. fetchTable()
  309. } catch (error) {
  310. console.error(error)
  311. }
  312. }
  313. fetchTable()
  314. </script>
  315. <style scoped lang="scss">
  316. .mark-score {
  317. color: red;
  318. font-size: 50px;
  319. left: 30px;
  320. top: 20px;
  321. }
  322. .header-view {
  323. border-bottom: $OnePixelLine;
  324. }
  325. .detail-info {
  326. height: 280px;
  327. .detail-info-chart {
  328. border-right: $OnePixelLine;
  329. }
  330. .detail-info-label {
  331. .detail-info-label-num {
  332. min-width: 32px;
  333. height: 24px;
  334. line-height: 24px;
  335. background: #00987b;
  336. border-radius: 4px;
  337. }
  338. }
  339. }
  340. .paper-view {
  341. .paper-img {
  342. max-width: 100%;
  343. }
  344. .preview {
  345. position: absolute;
  346. cursor: pointer;
  347. top: 10px;
  348. right: 20px;
  349. font-size: 24px;
  350. }
  351. }
  352. .paper-mark-record {
  353. width: 390px;
  354. }
  355. </style>