levelDistribution.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <template>
  2. <ReportModule :titleList="['1、分数段图']" tableOrChart="chart" :showPrintBtn="false" :showExportBtn="false">
  3. <template #title_right>
  4. <div :class="['right_item', { item_active: state.sectionScore == item }]" v-for="item in state.sortRangeScore"
  5. :key="item" @click="TagClick(item)">
  6. {{ item }}分段
  7. </div>
  8. <div class="right_set">
  9. <span>设置分数段</span>
  10. <el-input v-model="state.sectionScore" maxlength="3" style="width: 54px" @input="HandleInput"
  11. @change="BlurSectionScore" />
  12. <span>分/段</span>
  13. </div>
  14. <div class="right_radio">
  15. <el-select v-model="state.radioRangeScore" v-if="analysisStore?.filterObject?.classLevel != 2">
  16. <el-option :value="0" label="按年级"></el-option>
  17. <el-option :value="1" label="按班级"></el-option>
  18. </el-select>
  19. </div>
  20. </template>
  21. <template #module_table_chart>
  22. <template v-if="state.scoreSegmentData.datay.length">
  23. <template v-if="state.radioRangeScore == 0">
  24. <BarLineChart :datax="state.scoreSegmentData.datax" :datay="state.scoreSegmentData.datay"
  25. :fullScore="state.scoreSegmentData.fullScore" :markLine="state.scoreSegmentData.markLine"
  26. :color="state.scoreSegmentData.color" :title="state.scoreSegmentData.title"
  27. :tooltipData="state.scoreSegmentData.tooltipData" :unit="state.scoreSegmentData.unit"
  28. :tooltipTitle="state.scoreSegmentData.tooltipTitle">
  29. </BarLineChart>
  30. </template>
  31. <!-- 按班级 -->
  32. <template v-if="state.radioRangeScore == 1">
  33. <LineBarChart :datax="state.scoreSegmentClassData.datax" :datay="state.scoreSegmentClassData.datay"
  34. :showBackground="false" :legendList="state.scoreSegmentClassData.legendList"
  35. :title="state.scoreSegmentClassData.title" :tooltipData="state.scoreSegmentClassData.tooltipData"
  36. :hideOverlap="true"></LineBarChart>
  37. </template>
  38. </template>
  39. <div v-else class="no_content_data" v-loading="state.tableLoading" :element-loading-text="state.loadingText"
  40. element-loading-spinner="el-icon-loading" element-loading-background="#ffffff">
  41. <span>暂无数据</span>
  42. </div>
  43. </template>
  44. <template #module_describe>
  45. 说明:信度是反应考试一致性或可靠性的指标,信度高,表示考试成绩较为准确,误差较小;
  46. 信度低,表明误差大;信度低的考试无法正确评价考生的知识水平与智能素质。
  47. 效度是指测验的有效性或正确性,即测验能否准确测量出其所要测量的内容。效度高的测验能够确保测验内容与测验目的的一致性,反映测验的正确性和准确性。
  48. 分析试题的难度和区分度可以确保试题的质量和有效性,从而更好地评估学生的知识掌握程度和区分不同水平的学生。
  49. 难度指试题的难易程度,通常用P值表示。计算方式为P=X/M(P为难度,X为试题平均得分,M为试题满分)。P值在0到1之间,值越大表示试题越简单,试题通常分为容易题(P≥0.7)、中等题(0.4至0.7之间)和难题(P≤0.4)。
  50. 区分度是衡量试题对不同水平考生的区分能力,通常用D值表示。通过计算高分组和低分组在某一试题上的通过率之差,得到试题区分度。区分度高的试题能将不同水平的考生区分开来,水平高的学生得高分,水平低的学生得低分。D值的取值范围介于-1至1之间,D值越高,区分的效果越好。D≥0.4表明此题的区分度很好,属于优秀;0.3≤D<0.4表明此题的区分度较好,属于良好;0.2≤D<0.4表明此题的区分度一般;D<0.2表明此题的区分度较低。
  51. 图中展示了各科的命题分析明细,点击科目的柱可在下方查看该科所有小题的命题分析。
  52. </template>
  53. </ReportModule>
  54. <ReportModule :titleList="['2、分数段表']" tableOrChart="table" :showPrintBtn="false" :showDescribe="false"
  55. :currentPage="state.scoreSegmentData.pageNum" :pageSize="state.scoreSegmentData.pageSize"
  56. :total="state.scoreSegmentData.total" @update:pageSize="handleSizeChange" @update:currentPage="handleCurrentChange">
  57. <template #module_table_chart>
  58. <el-table :data="scoreRangeTableData" border>
  59. <template v-for="(header, headerIndex) in state.scoreSegmentData.headerData">
  60. <el-table-column align="center" :label="header.name" v-if="header.child && header.child.length"
  61. :key="`child_${headerIndex}`">
  62. <el-table-column v-for="(child, childIndex) in header.child" align="center" :label="child.value"
  63. :prop="child.prop" :key="`${headerIndex}_${childIndex}`" show-overflow-tooltip>
  64. <template #default="scope">
  65. <template v-for="(detailItem, detailKey) in scope.row.detailList">
  66. <span v-if="header.name == detailItem.schoolName" :key="`${headerIndex}_${childIndex}_${detailKey}`">
  67. <span :class="child.prop == 'doubleOnlineNum' ? 'table_row_blue' : ''"
  68. v-if="child.prop == 'doubleOnlineNum' && detailItem[child.prop] != 0 && detailItem[child.prop] != '-'">
  69. {{ detailItem[child.prop] }}
  70. </span>
  71. <span v-else>
  72. {{ detailItem[child.prop] }}
  73. </span>
  74. </span>
  75. </template>
  76. </template>
  77. </el-table-column>
  78. </el-table-column>
  79. <el-table-column v-else align="center" width="100" :label="header.name" :prop="header.prop" :key="headerIndex"
  80. fixed="left" show-overflow-tooltip>
  81. <template #default="scope">
  82. {{ scope.row[header.prop] }}
  83. </template>
  84. </el-table-column>
  85. </template>
  86. </el-table>
  87. </template>
  88. </ReportModule>
  89. </template>
  90. <script lang="ts" setup>
  91. import ReportModule from "@/components/ReportModule.vue";
  92. import BarLineChart from "@/components/echarts/barLineChart.vue";//柱状图折线图组件
  93. import LineBarChart from "@/components/echarts/lineBarChart.vue";//折线图柱状图组件
  94. import { useAnalysisStore } from "@/store/analysis";
  95. import { onMounted, reactive, computed, watch } from "vue";
  96. import { scoreSegment } from "@/api/analysis";
  97. const analysisStore = useAnalysisStore();
  98. const state = reactive({
  99. sectionScore: '', // 设置分数段输入框
  100. sortRangeScore: ['5', '10'], // 设置分数段
  101. radioRangeScore: 0, // 0按年级, 1按班级
  102. scoreSegmentData: {
  103. exportLoading: false,
  104. datax: [],
  105. datay: [],
  106. fullScore: 100, //满分值
  107. markLine: [], //辅助线
  108. tooltipData: [], //悬浮弹窗的数据
  109. title: ["年级", "年级"],
  110. color: ["#995FB3", "#5470C6"],
  111. unit: "人",
  112. tooltipTitle: "及格率",
  113. tableData: [],
  114. headerData: [],
  115. pageSize: 10, //每页显示数据
  116. total: 0, //总数
  117. pageNum: 1, //当前页
  118. }, //分数段图数据
  119. tableLoading: true,
  120. loadingText: "加载中……",
  121. scoreSegmentClassData: {
  122. datax: [],
  123. datay: [],
  124. title: [],
  125. legendList: [],
  126. tooltipData: [],//悬浮弹窗的数据
  127. }//分数段 按班级分析数据
  128. });
  129. const scoreRangeTableData = computed(() => {
  130. const { tableData, pageSize, pageNum } = state.scoreSegmentData;
  131. const start = (pageNum - 1) * pageSize;
  132. const end = start + pageSize;
  133. return tableData.slice(start, end);
  134. })
  135. //分数段
  136. const GetScoreSegment = async () => {
  137. state.tableLoading = true;
  138. const res = await scoreSegment({
  139. ...analysisStore.filterObject,
  140. scoreSegmentNum: state.sectionScore,
  141. });
  142. if (res.code === 200 && res.data && res.data.rowData && res.data.rowData.length) {
  143. let chartData = res.data.chartData
  144. let chartDataTotal = res.data.chartData.groupSchoolData // 联校/年级数据
  145. let chartDataSingle = res.data.chartData.oneSchoolData // 单校/班级数据
  146. let datax = []; // 分数段x轴数据
  147. let datay = []; // 联校/年级分数段y轴数据
  148. let count = []; // 联校/年级数据
  149. let tooltipData = []; // 联校/年级悬浮弹窗数据
  150. let markLine = []; // 辅助线数据
  151. let classTooltipData = []; //班级悬浮弹窗数据
  152. let classDatay = []; //分数段按班级y轴数据
  153. let classTitle = []; //分数段按班级图例数据
  154. let average = parseFloat(res.data.chartData.average); // 平均分
  155. let averageIndex = 0;//平均分的索引
  156. let standard = parseFloat(res.data.chartData.standard);//标准差
  157. let standardIndex = 0;//标准差索引
  158. let twoStandard = parseFloat(res.data.chartData.twoStandard);//2倍标准差
  159. let twoStandardIndex = 0;//2倍标准差索引
  160. let negativeOneStandard = parseFloat(res.data.chartData.negativeOneStandard);//-1倍标准差
  161. let negativeOneStandardIndex = 0;//-1倍标准差索引
  162. let negativeTwoStandard = parseFloat(res.data.chartData.negativeTwoStandard);//-2倍标准差
  163. let negativeTwoStandardIndex = 0;//-2倍标准差索引
  164. chartDataTotal.forEach((item, index) => {
  165. let list = item.name.split('-');
  166. let num1 = parseInt(list[0].replace(/[\[\]()]/g, ''));
  167. let num2 = parseInt(list[1].replace(/[\[\]()]/g, ''));
  168. if (num2 < average && average < num1) { // 平均分
  169. averageIndex = index
  170. }
  171. if (num2 < standard && standard < num1) { // 标准差
  172. standardIndex = index
  173. }
  174. if (num2 < twoStandard && twoStandard < num1) { // 2倍标准差
  175. twoStandardIndex = index
  176. }
  177. if (num2 < negativeOneStandard && negativeOneStandard < num1) { // -1倍标准差
  178. negativeOneStandardIndex = index
  179. }
  180. if (num2 < negativeTwoStandard && negativeTwoStandard < num1) { // -2倍标准差
  181. negativeTwoStandardIndex = index
  182. }
  183. datax.push(item.name)
  184. count.push(item.doubleOnlineNum)
  185. tooltipData.push({
  186. name: '',
  187. value: `${item.doubleOnlineNum}人,占比${item.doubleOnlineRate} ${item.studentUserName.length ? `(${item.studentUserName.join('、')})` : ''}`
  188. })
  189. });
  190. datay.push(count, count)
  191. markLine.push({
  192. name: '平均分',
  193. value: average,
  194. color: '#FAC858',
  195. xAxis: averageIndex,
  196. });
  197. markLine.push({
  198. name: '标准差',
  199. color: '#3BA272',
  200. value: standard,
  201. xAxis: standardIndex,
  202. });
  203. markLine.push({
  204. name: '-标准差',
  205. color: '#EE6666',
  206. value: negativeOneStandard,
  207. xAxis: negativeOneStandardIndex,
  208. });
  209. markLine.push({
  210. name: '-2倍标准差',
  211. color: '#EE6666',
  212. value: negativeTwoStandard,
  213. xAxis: negativeTwoStandardIndex,
  214. });
  215. markLine.push({
  216. name: '2倍标准差',
  217. color: '#3BA272',
  218. value: twoStandard,
  219. xAxis: twoStandardIndex,
  220. });
  221. //判断人数是否可点击
  222. const rowData = res.data.rowData || [];
  223. state.scoreSegmentData = {
  224. datax: datax,
  225. datay: datay,
  226. fullScore: parseFloat(chartData.fullScore),
  227. markLine: markLine,//辅助线数据
  228. tooltipData: tooltipData,//悬浮弹窗数据
  229. title: ["年级", "1班"],
  230. color: ["#995FB3", "#5470C6"],
  231. unit: '人',
  232. tooltipTitle: '及格率',
  233. tableData: rowData,
  234. headerData: res.data.titleData || [],
  235. pageSize: 10,//每页显示数据
  236. total: rowData.length,//总数
  237. pageNum: 1,//当前页
  238. };//分数段图数据
  239. // 单校/班级图表数据处理
  240. chartDataSingle.forEach(item => {
  241. classTitle.push(item.name)
  242. let singleItem = []
  243. let tootlipItem = []
  244. item.detailList.forEach(scoreItem => {
  245. singleItem.push(scoreItem.doubleOnlineNum)
  246. tootlipItem.push(`${scoreItem.doubleOnlineNum}人,占比${scoreItem.doubleOnlineRate}`);
  247. })
  248. classDatay.push(singleItem)
  249. classTooltipData.push(tootlipItem)
  250. })
  251. state.scoreSegmentClassData = {
  252. datax: datax,
  253. datay: classDatay,
  254. title: classTitle,//不会修改原数组
  255. legendList: classTitle.slice(0, 3),//默认显示全部
  256. tooltipData: classTooltipData,
  257. };
  258. } else {
  259. state.scoreSegmentData = {
  260. datax: [],
  261. datay: [],
  262. fullScore: 100,//满分值
  263. markLine: [],//辅助线
  264. tooltipData: [],//悬浮弹窗的数据
  265. title: ["年级", "年级"],
  266. color: ["#995FB3", "#5470C6"],
  267. unit: '人',
  268. tooltipTitle: '及格率',
  269. tableData: [],
  270. headerData: [],
  271. pageSize: 10,//每页显示数据
  272. total: 0,//总数
  273. pageNum: 1,//当前页
  274. };//分数段图数据
  275. state.scoreSegmentClassData = {
  276. datax: [],
  277. datay: [],
  278. title: [],
  279. legendList: [],
  280. tooltipData: [],//悬浮弹窗的数据
  281. };//分数段 按班级分析数据
  282. }
  283. state.tableLoading = false;
  284. };
  285. //分数段切换
  286. const TagClick = (item) => {
  287. state.sectionScore = item;
  288. GetScoreSegment();//获取分数段数据
  289. }
  290. const HandleInput = (value: string) => {
  291. state.sectionScore = value.replace(/[^\d]/g, '');
  292. }
  293. // 设置分数段失去焦点
  294. const BlurSectionScore = (value: string) => {
  295. if (value == '0') {
  296. state.sectionScore = '';
  297. }
  298. GetScoreSegment();//获取分数段数据
  299. }
  300. // 分页
  301. const handleCurrentChange = (val: number) => {
  302. state.scoreSegmentData.pageNum = val;
  303. }
  304. const handleSizeChange = (val: number) => {
  305. state.scoreSegmentData.pageSize = val;
  306. state.scoreSegmentData.pageNum = 1;
  307. }
  308. // 初始化
  309. const pageInit = () => {
  310. GetScoreSegment();
  311. };
  312. // 监听筛选条件
  313. watch(
  314. () => analysisStore.filterObject,
  315. async () => {
  316. state.sectionScore = '';
  317. pageInit();
  318. },
  319. { deep: true },
  320. );
  321. onMounted(() => {
  322. pageInit();
  323. });
  324. </script>
  325. <style lang="scss" scoped>
  326. .el-input__inner {
  327. height: 36px;
  328. }
  329. .el-input__suffix {
  330. line-height: 36px;
  331. }
  332. .right_item {
  333. font-size: 14px;
  334. color: #999;
  335. font-weight: 400;
  336. cursor: pointer;
  337. margin-right: 5px;
  338. border-bottom: 2px solid #ffffff;
  339. &.item_active {
  340. font-size: 14px;
  341. color: #2e64fa;
  342. font-weight: 500;
  343. cursor: pointer;
  344. border-bottom: 2px solid #2e64fa;
  345. }
  346. }
  347. .right_set {
  348. font-weight: 400;
  349. font-size: 14px;
  350. color: #333333;
  351. :deep(.el-input) {
  352. padding: 0 5px;
  353. margin: 0 2px;
  354. .el-input__inner {
  355. text-align: center;
  356. }
  357. }
  358. }
  359. .right_radio {
  360. :deep(.el-select) {
  361. width: 100px;
  362. }
  363. }
  364. </style>