Sfoglia il codice sorgente

feat: 评卷员明细统计页

chenhao 2 anni fa
parent
commit
6ed6e641d8

+ 1 - 1
src/api/api-types/statistics.d.ts

@@ -501,7 +501,7 @@ export namespace Statistics {
   }
 
   interface StatisticByMarker {
-    markerId?: number
+    markerId?: string | number
     endTime: string
     startTime: string
   }

+ 1 - 1
src/components/shared/message/Message.vue

@@ -1,6 +1,6 @@
 <template>
   <message-component>
-    <div ref="messageIcon" class="message-icon">
+    <div ref="messageIcon" v-bind="$attrs" class="message-icon">
       <span v-show="unReadMessages?.newCount" class="un-read-num">
         {{ Math.min(unReadMessages?.newCount || 0, 99) }}
       </span>

+ 7 - 4
src/components/shared/message/MessageSend.vue

@@ -98,13 +98,16 @@ const showSendPaper = computed<boolean>(() => {
 })
 
 const allowSend = computed<boolean>(() => {
-  return !!(messageContent.value && checkedUsers.value.length)
+  return !!(messageContent.value && checkedUsers.value?.filter((v) => !!v.id)?.length)
 })
 
 const treeRef = ref<InstanceType<typeof ElTree>>()
 
 const viewCheckedUser = computed(() => {
-  return checkedUsers?.value?.map((d) => `${d.loginName}-${d.name}`)?.join(';')
+  return checkedUsers?.value
+    ?.filter((v) => !!v.id)
+    ?.map((d) => `${d.loginName}-${d.name}`)
+    ?.join(';')
 })
 
 const toggleCheckUser = () => {
@@ -161,7 +164,7 @@ watch(
 )
 
 const onCheckChange = () => {
-  checkedUsers.value = (treeRef?.value?.getCheckedNodes(true) as MarkerItem[]) || []
+  checkedUsers.value = (treeRef?.value?.getCheckedNodes(true) as MarkerItem[]).filter((v) => !!v.id) || []
 }
 
 watch(messageContent, () => {
@@ -196,7 +199,7 @@ const onSendMessage = async () => {
     }
     await sendMessage({
       content: messageContent.value,
-      receiveUserIds: checkedUsers.value.map((u) => u.id),
+      receiveUserIds: checkedUsers.value?.filter(Boolean)?.map((u) => u.id),
     })
     ElMessage.success('发送成功')
     emit('close')

+ 1 - 1
src/modules/analysis/group-monitoring-detail/index.vue

@@ -2,7 +2,7 @@
   <div class="flex direction-column full">
     <div class="flex items-center p-extra-small fill-blank header-view">
       <el-button class="m-r-auto" size="small" plain @click="back()">返回</el-button>
-      <message :paper-path="current?.filePath"></message>
+      <message class="m-r-base" :paper-path="current?.filePath"></message>
       <user-info></user-info>
     </div>
     <div class="flex fill-blank detail-info">

+ 324 - 10
src/modules/analysis/marker-statistics/index.vue

@@ -1,20 +1,334 @@
 <template>
   <div class="full flex direction-column">
-    <div class="p-t-base p-l-base fill-blank">
-      <!-- <base-form :items="items" :model="model" :label-width="useVW(66)" size="small">
-        <template #form-item-button>
-          <el-button type="primary" @click="onSearch">查询</el-button>
-          <el-button special @click="onCreateExam">创建考试</el-button>
-        </template>
-      </base-form> -->
+    <div class="flex items-center p-extra-small fill-blank header-view">
+      <el-button class="m-r-auto" size="small" plain @click="back()">返回</el-button>
+      <message class="m-r-base"></message>
+      <user-info></user-info>
+    </div>
+    <div class="flex-1 overflow-hidden flex direction-column">
+      <div class="fill-blank p-t-medium-base filter-header">
+        <base-form size="small" :items="formItems" :model="model">
+          <template #form-item-search>
+            <el-button type="primary" @click="onSearch">刷新</el-button>
+          </template>
+        </base-form>
+      </div>
+      <div class="flex-1 flex overflow-hidden p-base">
+        <div class="flex-1 flex direction-column overflow-hidden">
+          <div class="fill-blank text-center p-t-base table-title">{{ query.markerName }}主观题给分分布</div>
+          <div class="flex-1 overflow-hidden fill-blank m-b-base table-box">
+            <base-table
+              size="small"
+              height="100%"
+              :columns="columns"
+              :data="subjectTableData"
+              @row-dblclick="onDbClick"
+            ></base-table>
+          </div>
+          <div class="flex-1 overflow-hidden radius-base fill-blank p-base chart-box">
+            <vue-echarts class="full" :option="markerSubjectiveChartsOption"></vue-echarts>
+          </div>
+        </div>
+        <div class="flex-1 flex direction-column overflow-hidden m-l-base">
+          <div class="fill-blank text-center p-t-base table-title">{{ query.markerName }}主观题给分分布</div>
+          <div class="flex-1 overflow-hidden fill-blank m-b-base table-box">
+            <base-table size="small" height="100%" :columns="columns" :data="objectiveTableData"></base-table>
+          </div>
+          <div class="flex-1 overflow-hidden radius-base fill-blank p-base chart-box">
+            <vue-echarts class="full" :option="markerObjectiveChartsOption"></vue-echarts>
+          </div>
+        </div>
+      </div>
     </div>
   </div>
 </template>
 
-<script setup lang="ts" name="MarkerStatistics">
+<script setup lang="tsx" name="MarkerStatistics">
 /** 评卷员明细统计 */
-import { reactive, ref } from 'vue'
+import { computed, reactive } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElButton } from 'element-plus'
+import dayjs from 'dayjs'
+import VueEcharts from 'vue-echarts'
+import Message from '@/components/shared/message/Message.vue'
+import UserInfo from '@/components/shared/UserInfo.vue'
+import BaseTable from '@/components/element/BaseTable.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
+import useFetch from '@/hooks/useFetch'
+
+import type { EChartsOption } from 'echarts'
+import type { ExtractApiResponse } from '@/api/api'
+import type { EpTableColumn, EpFormItem } from 'global-type'
+
+const { back, push } = useRouter()
+const { query } = useRoute()
+
+const model = reactive({
+  type: 'total',
+})
+
+const formItems: EpFormItem[] = [
+  {
+    label: '统计方式',
+    prop: 'type',
+    slotType: 'radio',
+    rowKey: 'row-1',
+    slot: {
+      options: [
+        { label: 'total', slotLabel: '积累' },
+        { label: 'today', slotLabel: '当天' },
+      ],
+    },
+    colProp: {
+      span: 3,
+      offset: 19,
+    },
+  },
+  {
+    slotName: 'search',
+    rowKey: 'row-1',
+    colProp: {
+      span: 2,
+    },
+  },
+]
+
+const { fetch: getStatisticObjectiveByMarker, result: objectiveByMarker } = useFetch('getStatisticObjectiveByMarker')
+const { fetch: getStatisticSubjectiveByMarker, result: subjectiveByMarker } = useFetch('getStatisticSubjectiveByMarker')
+
+type TableDataType = ExtractArrayValue<ExtractApiResponse<'getStatisticObjectiveByMarker'>['segmentsByAll']> & {
+  groupCount: number
+  groupRate: number
+  allCount: number
+  allRate: number
+}
+
+type GroupData = Record<number, { groupCount: number; groupRate: number }>
+type AllData = Record<number, { allCount: number; allRate: number }>
+
+const objectiveGroupData = computed<GroupData>(() => {
+  return objectiveByMarker.value?.segmentsByGroup?.reduce((result, data) => {
+    result[data.scoreStart] = { groupCount: data.count, groupRate: data.rate }
+    return result
+  }, {} as GroupData)
+})
+
+const objectiveAllData = computed<AllData>(() => {
+  return objectiveByMarker.value?.segmentsByAll?.reduce((result, data) => {
+    result[data.scoreStart] = { allCount: data.count, allRate: data.rate }
+    return result
+  }, {} as AllData)
+})
+
+const subjectiveGroupData = computed<GroupData>(() => {
+  return subjectiveByMarker.value?.segmentsByGroup?.reduce((result, data) => {
+    result[data.scoreStart] = { groupCount: data.count, groupRate: data.rate }
+    return result
+  }, {} as GroupData)
+})
+
+const subjectiveAllData = computed<AllData>(() => {
+  return subjectiveByMarker.value?.segmentsByAll?.reduce((result, data) => {
+    result[data.scoreStart] = { allCount: data.count, allRate: data.rate }
+    return result
+  }, {} as AllData)
+})
+
+const objectiveTableData = computed<TableDataType[]>(() => {
+  return objectiveByMarker?.value?.segmentsByUser.map((d) => ({
+    ...d,
+    ...objectiveGroupData.value?.[d.scoreStart],
+    ...objectiveAllData.value?.[d.scoreStart],
+  }))
+})
+
+const subjectTableData = computed<TableDataType[]>(() => {
+  return subjectiveByMarker?.value?.segmentsByUser.map((d) => ({
+    ...d,
+    ...subjectiveGroupData.value?.[d.scoreStart],
+    ...subjectiveAllData.value?.[d.scoreStart],
+  }))
+})
+
+const columns: EpTableColumn<TableDataType>[] = [
+  { label: '分数', prop: 'scoreStart' },
+  { label: '人数', prop: 'count' },
+  { label: '百分比', prop: 'rate', formatter: (row) => `${row.rate}%` },
+  { label: '本组人数', prop: 'groupCount' },
+  { label: '百分比', prop: 'groupRate', formatter: (row) => `${row.groupRate}%` },
+  { label: '全体人数', prop: 'allCount' },
+  { label: '百分比', prop: 'allRate', formatter: (row) => `${row.allRate}%` },
+]
+
+const onSearch = () => {
+  if (query.markerId) {
+    const startTime = model.type === 'today' ? dayjs().startOf('day').format('YYYYMMDDHHmmss') : ''
+    getStatisticObjectiveByMarker({
+      markerId: query.markerId as string,
+      startTime,
+      endTime: '',
+    })
+    getStatisticSubjectiveByMarker({
+      markerId: query.markerId as string,
+      startTime,
+      endTime: '',
+    })
+  }
+}
+
+const onDbClick = (row: TableDataType) => {
+  if (query.markerId) {
+    push({
+      name: 'AnalysisViewMarked',
+      params: {
+        markerId: query.markerId as string,
+      },
+      query: {
+        markerName: query.markerName,
+        score: row.scoreStart,
+      },
+    })
+  }
+}
+
+type StatisticObjectiveByMarker = ExtractApiResponse<'getStatisticObjectiveByMarker'>
+
+type StatisticObjectiveByMarkerValues = StatisticObjectiveByMarker['segmentsByAll']
+
+const getXAxisData = <K extends keyof ExtractArrayValue<StatisticObjectiveByMarkerValues>>(
+  field: K,
+  data?: StatisticObjectiveByMarkerValues
+) => {
+  if (!data) {
+    return []
+  }
+  const getValue = (key: K, item: ExtractArrayValue<StatisticObjectiveByMarkerValues>) => {
+    return item[key]
+  }
+  return data?.map((v) => getValue(field, v))
+}
+
+const markerSubjectiveChartsOption = computed<EChartsOption>(() => {
+  return {
+    legend: {
+      itemWidth: 14,
+      data: ['评卷员主观分布', '小组主观分布', '题组主观分布'],
+    },
+    xAxis: {
+      axisLine: { show: false },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: {
+        align: 'right',
+      },
+      data: getXAxisData('scoreStart', subjectiveByMarker?.value?.segmentsByAll),
+    },
+    yAxis: [
+      {
+        type: 'value',
+      },
+      {
+        type: 'value',
+        axisLabel: {
+          formatter: `{value}%`,
+        },
+        splitLine: { show: false },
+      },
+    ],
+    series: [
+      {
+        name: '评卷员主观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#3AD500',
+        },
+        data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByUser),
+      },
+      {
+        name: '小组主观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#0064FF',
+        },
+        data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByGroup),
+      },
+      {
+        name: '题组主观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#008000',
+        },
+        data: getXAxisData('rate', subjectiveByMarker?.value?.segmentsByAll),
+      },
+    ],
+  }
+})
+
+const markerObjectiveChartsOption = computed<EChartsOption>(() => {
+  return {
+    legend: {
+      itemWidth: 14,
+      data: ['评卷员客观分布', '小组客观分布', '题组客观分布'],
+    },
+    xAxis: {
+      axisLine: { show: false },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: {
+        align: 'right',
+      },
+      data: getXAxisData('scoreStart', objectiveByMarker?.value?.segmentsByAll),
+    },
+    yAxis: [
+      {
+        type: 'value',
+      },
+      {
+        type: 'value',
+        axisLabel: {
+          formatter: `{value}%`,
+        },
+        splitLine: { show: false },
+      },
+    ],
+    series: [
+      {
+        name: '评卷员客观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#3AD500',
+        },
+        data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByUser),
+      },
+      {
+        name: '小组客观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#0064FF',
+        },
+        data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByGroup),
+      },
+      {
+        name: '题组客观分布',
+        type: 'line',
+        itemStyle: {
+          color: '#008000',
+        },
+        data: getXAxisData('rate', objectiveByMarker?.value?.segmentsByAll),
+      },
+    ],
+  }
+})
+
+onSearch()
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.filter-header {
+  border-top: 1px solid #eee;
+  border-bottom: 1px solid #eee;
+}
+.chart-box {
+  height: 350px;
+}
+</style>

+ 15 - 2
src/modules/analysis/personnel-statistics/components/StatisticsPersonnel.vue

@@ -8,6 +8,7 @@
       :height="useVW(300)"
       highlight-current-row
       @current-change="onCurrentChange"
+      @row-dblclick="onDbClick"
     >
       <template #column-marker="{ row }">
         <el-popover
@@ -48,7 +49,8 @@
 
 <script setup lang="tsx" name="StatisticsPersonnel">
 /** 人员数据统计-按人员展开 */
-import { reactive, ref, inject, computed, watch } from 'vue'
+import { ref, inject, computed, watch } from 'vue'
+import { useRouter } from 'vue-router'
 import { ElButton, ElPopover, ElMenu, ElMenuItem } from 'element-plus'
 import VueECharts from 'vue-echarts'
 import BaseTable from '@/components/element/BaseTable.vue'
@@ -70,6 +72,8 @@ const props = defineProps<{
 const setMessageVisible = inject<(visible: boolean) => void>('setMessageVisible')
 const setReplyUserId = inject<(id: number) => void>('setReplyUserId')
 
+const { push } = useRouter()
+
 const columns: EpTableColumn<ExtractArrayValue<ExtractApiResponse<'getStatisticsByGroup'>>>[] = [
   {
     label: '小组',
@@ -144,12 +148,21 @@ const data = computed(() => {
   return props.data || []
 })
 
-const { tableRef, tableData, current, onCurrentChange } = useTableCheck(data)
+const { tableRef, tableData, current, onCurrentChange, onDbClick, currentView } = useTableCheck(data)
 
 watch(tableData, () => {
   popovers.value = {}
 })
 
+watch(currentView, () => {
+  if (currentView.value) {
+    push({
+      name: 'AnalysisPersonnelStatisticsMarker',
+      query: { markerId: currentView.value.markerId, markerName: currentView.value.markerName },
+    })
+  }
+})
+
 const { fetch: getStatisticObjectiveByMarker, result: objectiveByMarker } = useFetch('getStatisticObjectiveByMarker')
 const { fetch: getStatisticSubjectiveByMarker, result: subjectiveByMarker } = useFetch('getStatisticSubjectiveByMarker')
 

+ 20 - 11
src/modules/marking/repeat/index.vue

@@ -5,7 +5,15 @@
       :paper-path="currentReMarkPaper?.filePath"
       @click="onOperationClick"
     >
-      <el-button class="m-l-base m-r-auto" size="small" type="primary" @click="onConfirmReMark">确认</el-button>
+      <el-button
+        class="m-l-base m-r-auto"
+        size="small"
+        type="primary"
+        :disabled="!currentReMarkPaper"
+        @click="onConfirmReMark"
+      >
+        确认
+      </el-button>
     </mark-header>
     <div class="flex flex-1 overflow-hidden p-base mark-container">
       <div
@@ -119,15 +127,6 @@ const onPreview = () => {
   previewModalVisible.value = true
 }
 
-const { fetch: confirmReMarkPaper } = useFetch('confirmReMarkPaper')
-
-/** 确认重评 */
-const onConfirmReMark = () => {
-  if (currentReMarkPaper.value?.id) {
-    confirmReMarkPaper({ id: currentReMarkPaper.value.id }).then(checkNext)
-  }
-}
-
 type OperationClick = MarkHeaderInstance['onClick']
 
 type OperationType = Parameters<Exclude<OperationClick, undefined>>[0]['type']
@@ -215,7 +214,17 @@ const onSearch = async () => {
   getReMarkPaperList(formModel)
 }
 
-/** 确认 */
+const { fetch: confirmReMarkPaper } = useFetch('confirmReMarkPaper')
+
+/** 确认重评 */
+const onConfirmReMark = async () => {
+  if (currentReMarkPaper.value?.id) {
+    await confirmReMarkPaper({ id: currentReMarkPaper.value.id })
+    await onSearch()
+  }
+}
+
+/** 给分 */
 const { fetch: markReMarkPaper } = useFetch('markReMarkPaper')
 
 const onSubmit = async () => {

+ 10 - 10
src/router/analysis.ts

@@ -48,16 +48,6 @@ const routes: RouteRecordRaw[] = [
           sort: 3,
         },
       },
-      {
-        name: 'AnalysisPersonnelStatisticsMarker',
-        path: '/analysis/marker-statistics',
-        component: () => import('@/modules/analysis/marker-statistics/index.vue'),
-        meta: {
-          label: '评卷员明细统计',
-          menu: false,
-          menuId: 'analysis-personnel_statistics-marker',
-        },
-      },
       {
         name: 'AnalysisMarkingProgress',
         path: 'marking-progress',
@@ -82,6 +72,16 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    name: 'AnalysisPersonnelStatisticsMarker',
+    path: '/analysis/marker-statistics',
+    component: () => import('@/modules/analysis/marker-statistics/index.vue'),
+    meta: {
+      label: '评卷员明细统计',
+      menu: false,
+      menuId: 'analysis-personnel_statistics-marker',
+    },
+  },
   {
     name: 'AnalysisGroupDetail',
     path: '/analysis/group-detail',