|
@@ -1,20 +1,334 @@
|
|
<template>
|
|
<template>
|
|
<div class="full flex direction-column">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</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 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>
|
|
</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>
|