Browse Source

feat: 报告分析

zhangjie 6 tháng trước cách đây
mục cha
commit
9a9bb0df65

+ 2 - 0
package.json

@@ -20,6 +20,7 @@
     "core-js": "^3.8.3",
     "crypto-js": "^4.1.1",
     "docx-preview": "^0.3.2",
+    "echarts": "^5.5.1",
     "element-ui": "2.15.6",
     "html2canvas": "^1.4.1",
     "html2pdf.js": "^0.10.2",
@@ -30,6 +31,7 @@
     "vue": "^2.6.12",
     "vue-awesome": "^4.1.0",
     "vue-bus": "^1.2.1",
+    "vue-echarts": "^7.0.3",
     "vue-router": "^3.5.1",
     "vuex": "^3.6.2"
   },

+ 0 - 1
src/assets/styles/base.scss

@@ -214,7 +214,6 @@ body {
 
 .part-box-title {
   font-size: 20px;
-  min-height: 40px;
   line-height: 1;
   padding-bottom: 20px;
   border-bottom: 1px solid $--color-border;

+ 47 - 0
src/assets/styles/pages.scss

@@ -109,6 +109,11 @@
   }
 }
 // .property-info
+.property-info {
+  .part-box-title {
+    min-height: 40px;
+  }
+}
 .property-box {
   padding: 20px;
 }
@@ -1724,3 +1729,45 @@
     }
   }
 }
+
+// statistics-dialog
+.statistics-dialog {
+  &.el-dialog {
+    background-color: #fff;
+  }
+  .el-table th.el-table__cell {
+    background-color: #f5f7fa;
+  }
+  .statistics {
+    width: 1200px;
+    margin: 0 auto;
+  }
+  .statistics-part {
+    margin-bottom: 30px;
+  }
+  .statistics-head {
+    margin-bottom: 15px;
+    > h2 {
+      font-size: 16px;
+      font-weight: 600;
+      line-height: 1;
+      margin: 0;
+      color: #333;
+    }
+  }
+
+  .stat-type {
+    &-body {
+      display: flex;
+      align-items: stretch;
+    }
+
+    &-table {
+      width: 600px;
+    }
+    &-chart {
+      width: 600px;
+      height: 200px;
+    }
+  }
+}

+ 1 - 0
src/main.js

@@ -15,6 +15,7 @@ import "./assets/styles/index.scss";
 import "./modules/card/assets/styles/module.scss";
 import "./modules/paper-export/assets/styles/module.scss";
 import "@/components/svgIcon/svgIcon";
+import "./plugins/VueCharts";
 
 import globalVuePlugins from "./plugins/globalVuePlugins";
 Vue.use(globalVuePlugins);

+ 24 - 0
src/modules/paper/api.js

@@ -127,6 +127,30 @@ export const paperBlueInfoApi = ({ paperId, courseId, rootOrgId }) => {
     },
   });
 };
+export const paperQuestionTypeInfoApi = ({ paperId, rootOrgId }) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/paper/statistic/paper_detail`,
+    {},
+    {
+      params: {
+        id: paperId,
+        rootOrgId,
+      },
+    }
+  );
+};
+export const paperDifficultTypeInfoApi = ({ paperId, rootOrgId }) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/paper/statistic/difficult`,
+    {},
+    {
+      params: {
+        id: paperId,
+        rootOrgId,
+      },
+    }
+  );
+};
 export const paperQtypeInfoApi = ({ paperId, type }) => {
   return $httpWithMsg.get(`${QUESTION_API}/paper/question/type`, {
     params: {

+ 172 - 133
src/modules/paper/components/PaperStructInfo.vue

@@ -1,68 +1,22 @@
 <template>
   <el-dialog
+    custom-class="statistics-dialog"
     title="试卷结构分析"
     :visible.sync="modalIsShow"
     :modal="true"
     fullscreen
     append-to-body
-    custom-class="side-dialog"
     @open="getData"
   >
-    <el-form label-width="90px">
-      <el-form-item label="整卷属性">
-        <div class="topic-set-list">
-          <div
-            v-for="(paperTag, tagIndex) in paperData.tags"
-            :key="tagIndex"
-            class="topic-set"
-          >
-            <div class="topic-set-title">{{ paperTag.tag }}</div>
-            <div class="topic-set-content">
-              {{ paperTag.content }}
-            </div>
-          </div>
+    <div class="statistics">
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>组成结构</h2>
         </div>
-      </el-form-item>
-      <el-form-item label="组成结构">
-        <el-table :data="paperData.data" border>
-          <el-table-column
-            v-for="(colval, colIndex) in paperData.head"
-            :key="colIndex"
-          >
-            <template slot="header">
-              <span style="margin-left: 10px">{{ colval }}</span>
-            </template>
-            <template slot-scope="scope">
-              <span style="margin-left: 10px">{{ scope.row[colIndex] }}</span>
-            </template>
-          </el-table-column>
-        </el-table>
-      </el-form-item>
-    </el-form>
-
-    <!-- <el-form>
-      <el-form-item label="统计维度">
-        <el-select v-model="type" @change="getPaperData">
-          <el-option label="题数" value="count"></el-option>
-          <el-option label="分值" value="score"></el-option>
-        </el-select>
-      </el-form-item>
-    </el-form> -->
-    <el-form label-width="90px" style="margin-top: 40px">
-      <el-form-item label="题型分布">
-        <el-select v-model="type" @change="getPaperData">
-          <el-option label="题数" value="count"></el-option>
-          <el-option label="分值" value="score"></el-option>
-        </el-select>
-        <el-table
-          :data="paperData2.data"
-          border
-          :span-method="objectSpanMethod"
-          style="margin-top: 10px"
-        >
-          <el-table-column label="题型" align="center">
+        <div class="statistics-body">
+          <el-table :data="paperData.data" border>
             <el-table-column
-              v-for="(colval, colIndex) in headFirst2"
+              v-for="(colval, colIndex) in paperData.head"
               :key="colIndex"
             >
               <template slot="header">
@@ -72,50 +26,94 @@
                 <span style="margin-left: 10px">{{ scope.row[colIndex] }}</span>
               </template>
             </el-table-column>
-          </el-table-column>
-          <el-table-column label="大题" align="center">
-            <el-table-column
-              v-for="(colval, colIndex) in headSecond2"
-              :key="colIndex"
-            >
-              <template slot="header">
-                <span style="margin-left: 10px">{{ colval }}</span>
-              </template>
-              <template slot-scope="scope">
-                <span style="margin-left: 10px">{{
-                  scope.row[colIndex + 2]
-                }}</span>
-              </template>
-            </el-table-column>
-          </el-table-column>
-        </el-table>
-      </el-form-item>
-    </el-form>
+          </el-table>
+        </div>
+      </div>
+
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按题型统计</h2>
+        </div>
+        <div class="statistics-body stat-type-body">
+          <div class="stat-type-table">
+            <el-table :data="paperQuestionData" border>
+              <el-table-column label="题型" prop="paperDetailName" width="120">
+              </el-table-column>
+              <el-table-column label="试题数量">
+                <template slot-scope="scope">
+                  <span>
+                    {{ scope.row.questionCount }} (
+                    {{
+                      scope.row.questionDifficultInfo
+                        .map(
+                          (item) =>
+                            `${item.difficultLevel}:${item.questionCount}`
+                        )
+                        .join(",")
+                    }})
+                  </span>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div class="stat-type-chart">
+            <v-chart
+              v-if="paperQuestionChartOption"
+              :option="paperQuestionChartOption"
+            ></v-chart>
+          </div>
+        </div>
+      </div>
 
-    <!-- <h2 style="color: #262626; margin-bottom: 10px">知识点</h2> -->
-    <!-- <el-form>
-      <el-form-item label="可选属性">
-        <property-select
-          v-model="coursePropertyId"
-          :course-id="courseId"
-          @change="getPaperData"
-        >
-        </property-select>
-      </el-form-item>
-    </el-form> -->
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按难度统计</h2>
+        </div>
+        <div class="statistics-body stat-type-body">
+          <div class="stat-type-table">
+            <el-table :data="paperDifficultData" border>
+              <el-table-column label="难度" prop="difficultLevel" width="80">
+              </el-table-column>
+              <el-table-column label="试题数量">
+                <template slot-scope="scope">
+                  <span>
+                    {{ scope.row.questionCount }} (
+                    {{
+                      scope.row.paperDetailInfo
+                        .map(
+                          (item) =>
+                            `${item.paperDetailName}:${item.questionCount}`
+                        )
+                        .join(",")
+                    }})
+                  </span>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div class="stat-type-chart">
+            <v-chart
+              v-if="paperDifficultChartOption"
+              :option="paperDifficultChartOption"
+            ></v-chart>
+          </div>
+        </div>
+      </div>
 
-    <el-form label-width="90px" style="margin-top: 40px">
-      <el-form-item label="蓝图分布">
-        <el-table
-          :data="paperData3.data"
-          style="width: 100%"
-          border
-          :span-method="objectSpanMethod3"
-        >
-          <el-table-column label="蓝图属性" align="center">
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按知识点统计</h2>
+        </div>
+        <div class="statistics-body">
+          <el-table
+            :data="paperData3.data"
+            style="width: 100%"
+            border
+            :span-method="objectSpanMethod3"
+          >
             <el-table-column
               v-for="(colval, colIndex) in headFirst3"
-              :key="colIndex"
+              :key="`first${colIndex}`"
             >
               <template slot="header">
                 <span style="margin-left: 10px">{{ colval }}</span>
@@ -124,11 +122,9 @@
                 <span style="margin-left: 10px">{{ scope.row[colIndex] }}</span>
               </template>
             </el-table-column>
-          </el-table-column>
-          <el-table-column label="大题" align="center">
             <el-table-column
               v-for="(colval, colIndex) in headSecond3"
-              :key="colIndex"
+              :key="`second${colIndex}`"
             >
               <template slot="header">
                 <span style="margin-left: 10px">{{ colval }}</span>
@@ -139,15 +135,20 @@
                 }}</span>
               </template>
             </el-table-column>
-          </el-table-column>
-        </el-table>
-      </el-form-item>
-    </el-form>
+          </el-table>
+        </div>
+      </div>
+    </div>
   </el-dialog>
 </template>
 
 <script>
-import { paperBaseInfoApi, paperQtypeInfoApi, paperBlueInfoApi } from "../api";
+import {
+  paperBaseInfoApi,
+  paperQuestionTypeInfoApi,
+  paperDifficultTypeInfoApi,
+  paperBlueInfoApi,
+} from "../api";
 export default {
   name: "PaperStructInfo",
   props: {
@@ -168,9 +169,10 @@ export default {
         head: [],
       },
       type: "count",
-      paperData2: { head: [] },
-      headFirst2: [],
-      headSecond2: [],
+      paperQuestionData: [],
+      paperQuestionChartOption: null,
+      paperDifficultData: [],
+      paperDifficultChartOption: null,
       paperData3: { head: [], data: [] },
       headFirst3: [],
       headSecond3: [],
@@ -190,40 +192,77 @@ export default {
       this.getPaperData3();
     },
     async getPaperData() {
-      const res = await paperQtypeInfoApi({
+      const qres = await paperQuestionTypeInfoApi({
         paperId: this.paperId,
-        type: this.type,
+        rootOrgId: this.$store.state.user.rootOrgId,
       });
-      this.paperData2 = res.data;
-      this.headFirst2 = this.paperData2.head.slice(0, 2);
-      this.headSecond2 = this.paperData2.head.slice(2);
-    },
-    objectSpanMethod({ rowIndex, columnIndex }) {
-      // console.log(row, column);
-      if (columnIndex === 0 && rowIndex === 0) {
-        return {
-          rowspan: 5,
-          colspan: 1,
-        };
-      }
-      if (columnIndex === 0 && rowIndex > 0 && rowIndex < 5) {
+      this.paperQuestionData = qres.data;
+      const qData = this.paperQuestionData.map((item) => {
         return {
-          rowspan: 0,
-          colspan: 0,
+          value: item.questionCount,
+          name: item.paperDetailName,
         };
-      }
-      if (columnIndex === 0 && rowIndex === 5) {
-        return {
-          rowspan: 4,
-          colspan: 1,
-        };
-      }
-      if (columnIndex === 0 && rowIndex > 5 && rowIndex < 9) {
+      });
+      this.paperQuestionChartOption = this.getChartOption(qData);
+
+      const dres = await paperDifficultTypeInfoApi({
+        paperId: this.paperId,
+        rootOrgId: this.$store.state.user.rootOrgId,
+      });
+      this.paperDifficultData = dres.data;
+
+      const dData = this.paperDifficultData.map((item) => {
         return {
-          rowspan: 0,
-          colspan: 0,
+          value: item.questionCount,
+          name: item.difficultLevel,
         };
-      }
+      });
+      this.paperDifficultChartOption = this.getChartOption(dData);
+    },
+    getChartOption(data) {
+      const option = {
+        color: [
+          "#265dff",
+          "#14c9c8",
+          "#f7ba1f",
+          "#722ed1",
+          "#5470c6",
+          "#91cc75",
+          "#fac858",
+          "#ee6666",
+          "#73c0de",
+          "#3ba272",
+          "#fc8452",
+          "#9a60b4",
+          "#ea7ccc",
+        ],
+        tooltip: {
+          trigger: "item",
+        },
+        legend: {
+          orient: "vertical",
+          left: "right",
+          itemHeight: 10,
+        },
+        series: [
+          {
+            name: "Access From",
+            type: "pie",
+            radius: "90%",
+            data,
+            label: { show: false },
+            emphasis: {
+              itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: "rgba(0, 0, 0, 0.5)",
+              },
+            },
+          },
+        ],
+      };
+
+      return option;
     },
     async getPaperData3() {
       // if (!this.coursePropertyId) return;

+ 9 - 9
src/modules/paper/views/BuildPaper.vue

@@ -82,7 +82,7 @@
                 range-separator="至"
                 start-placeholder="开始时间"
                 end-placeholder="结束时间"
-                value-format="timestamp"
+                value-format="yyyy/MM/dd HH:mm:ss"
                 align="right"
                 unlink-panels
                 @change="dateChange"
@@ -221,8 +221,8 @@ const initModalForm = {
   maxLimit: 0,
   topicOnly: false,
   timeLimit: 1,
-  createStartTime: undefined,
-  createEndTime: undefined,
+  startTime: undefined,
+  endTime: undefined,
 };
 
 export default {
@@ -312,17 +312,17 @@ export default {
     modelTypeChange() {
       if (this.genModelType === "manual") {
         this.createTime = [];
-        this.modalForm.createStartTime = undefined;
-        this.modalForm.createEndTime = undefined;
+        this.modalForm.startTime = undefined;
+        this.modalForm.endTime = undefined;
       }
     },
     dateChange() {
       if (this.createTime) {
-        this.modalForm.createStartTime = this.createTime[0];
-        this.modalForm.createEndTime = this.createTime[1];
+        this.modalForm.startTime = this.createTime[0];
+        this.modalForm.endTime = this.createTime[1];
       } else {
-        this.modalForm.createStartTime = undefined;
-        this.modalForm.createEndTime = undefined;
+        this.modalForm.startTime = undefined;
+        this.modalForm.endTime = undefined;
       }
     },
     async confirm() {

+ 45 - 0
src/modules/statistics/api.js

@@ -15,3 +15,48 @@ export const statisticsExportApi = (data) => {
     }
   );
 };
+
+export const questionTypeInfoApi = (courseId) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/statistic/distribution`,
+    {},
+    {
+      params: {
+        courseId,
+      },
+    }
+  );
+};
+export const questionDifficultInfoApi = (courseId) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/statistic/difficult`,
+    {},
+    {
+      params: {
+        courseId,
+      },
+    }
+  );
+};
+
+export const courseBlueInfoApi = (courseId) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/statistic/property_distribution`,
+    {},
+    {
+      params: {
+        courseId,
+      },
+    }
+  );
+};
+
+export const coursePaperInfoApi = (data) => {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/course/page/paper/statistic`,
+    {},
+    {
+      params: data,
+    }
+  );
+};

+ 99 - 0
src/modules/statistics/components/StatisticsPaperDialog.vue

@@ -0,0 +1,99 @@
+<template>
+  <el-dialog
+    custom-class="statistics-dialog"
+    title="试卷分析"
+    :visible.sync="modalIsShow"
+    :modal="true"
+    fullscreen
+    append-to-body
+    @open="toPage(1)"
+  >
+    <div class="statistics">
+      <el-table
+        v-loading="loading"
+        element-loading-text="加载中"
+        :data="tableData"
+      >
+        试卷名称
+        <el-table-column prop="paperName" label="试卷名称"> </el-table-column>
+        <el-table-column prop="totalScore" label="试卷总分" width="100">
+        </el-table-column>
+        <el-table-column prop="difficult" label="难度" width="100">
+        </el-table-column>
+        <el-table-column prop="paperDetailCount" label="大题数" width="100">
+        </el-table-column>
+        <el-table-column prop="unitCount" label="小题数" width="100">
+        </el-table-column>
+        <el-table-column prop="creatorName" label="创建人" width="120">
+        </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="170">
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          :current-page.sync="currentPage"
+          :page-size.sync="pageSize"
+          :page-sizes="[10, 20, 50, 100, 200, 300]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @current-change="toPage"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { coursePaperInfoApi } from "../api";
+
+export default {
+  name: "StatisticsPaperDialog",
+  props: {
+    courseId: {
+      type: [String, Number],
+      default: null,
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      loading: false,
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    handleSizeChange(val) {
+      this.currentPage = 1;
+      this.pageSize = val;
+      this.getList();
+    },
+    toPage(val) {
+      this.currentPage = val;
+      this.getList();
+    },
+    async getList() {
+      this.tableData = [];
+      this.loading = true;
+      const res = await coursePaperInfoApi({
+        courseId: this.courseId,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      }).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+      this.total = res.data.totalElements;
+      this.tableData = res.data.content || [];
+    },
+  },
+};
+</script>

+ 317 - 0
src/modules/statistics/components/StatisticsQuestionDialog.vue

@@ -0,0 +1,317 @@
+<template>
+  <el-dialog
+    custom-class="statistics-dialog"
+    title="试题分析"
+    :visible.sync="modalIsShow"
+    :modal="true"
+    fullscreen
+    append-to-body
+    @open="getData"
+  >
+    <div class="statistics">
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按题型统计</h2>
+        </div>
+        <div class="statistics-body stat-type-body">
+          <div class="stat-type-table">
+            <el-table :data="paperQuestionData" border>
+              <el-table-column label="题型" prop="sourceDetailName" width="120">
+              </el-table-column>
+              <el-table-column label="试题数量">
+                <template slot-scope="scope">
+                  <span>
+                    {{ scope.row.questionCount }} (
+                    {{
+                      scope.row.questionDifficultInfo
+                        .map(
+                          (item) =>
+                            `${item.difficultLevel}:${item.questionCount}`
+                        )
+                        .join(",")
+                    }})
+                  </span>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div class="stat-type-chart">
+            <v-chart
+              v-if="paperQuestionChartOption"
+              :option="paperQuestionChartOption"
+            ></v-chart>
+          </div>
+        </div>
+      </div>
+
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按难度统计</h2>
+        </div>
+        <div class="statistics-body stat-type-body">
+          <div class="stat-type-table">
+            <el-table :data="paperDifficultData" border>
+              <el-table-column label="难度" prop="difficultLevel" width="80">
+              </el-table-column>
+              <el-table-column label="试题数量">
+                <template slot-scope="scope">
+                  <span>
+                    {{ scope.row.questionCount }} (
+                    {{
+                      scope.row.questionTypeInfo
+                        .map(
+                          (item) =>
+                            `${item.sourceDetailName}:${item.questionCount}`
+                        )
+                        .join(",")
+                    }})
+                  </span>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div class="stat-type-chart">
+            <v-chart
+              v-if="paperDifficultChartOption"
+              :option="paperDifficultChartOption"
+            ></v-chart>
+          </div>
+        </div>
+      </div>
+
+      <div class="statistics-part">
+        <div class="statistics-head">
+          <h2>按知识点统计</h2>
+        </div>
+        <div class="statistics-body">
+          <el-table :data="blueData" border :span-method="objectSpanMethod">
+            <el-table-column
+              label="一级知识点"
+              prop="firstProperty"
+            ></el-table-column>
+            <el-table-column
+              label="二级知识点"
+              prop="secondProperty"
+            ></el-table-column>
+            <el-table-column
+              v-for="(colval, colIndex) in questionTypeList"
+              :key="`${colval}${colIndex}`"
+              :prop="colval"
+            >
+              <template slot-scope="scope">
+                <span>{{ scope.row[colval] || "--" }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import {
+  questionTypeInfoApi,
+  questionDifficultInfoApi,
+  courseBlueInfoApi,
+} from "../api";
+
+export default {
+  name: "StatisticsQuestionDialog",
+  props: {
+    courseId: {
+      type: [String, Number],
+      default: null,
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      paperQuestionData: [],
+      paperQuestionChartOption: null,
+      paperDifficultData: [],
+      paperDifficultChartOption: null,
+      blueData: [],
+      questionTypeList: [],
+      rowspans: [],
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    getData() {
+      this.paperQuestionChartOption = null;
+      this.paperDifficultChartOption = null;
+      this.rowspans = [];
+      this.questionTypeList = [];
+      this.getQuestionData();
+      this.getBlueData();
+    },
+    async getQuestionData() {
+      const qres = await questionTypeInfoApi(this.courseId);
+      this.paperQuestionData = qres.data;
+      const qData = this.paperQuestionData.map((item) => {
+        return {
+          value: item.questionCount,
+          name: item.sourceDetailName,
+        };
+      });
+      this.paperQuestionChartOption = this.getChartOption(qData);
+
+      const dres = await questionDifficultInfoApi(this.courseId);
+      this.paperDifficultData = dres.data;
+
+      const dData = this.paperDifficultData.map((item) => {
+        return {
+          value: item.questionCount,
+          name: item.difficultLevel,
+        };
+      });
+      this.paperDifficultChartOption = this.getChartOption(dData);
+    },
+    getChartOption(data) {
+      const option = {
+        color: [
+          "#265dff",
+          "#14c9c8",
+          "#f7ba1f",
+          "#722ed1",
+          "#5470c6",
+          "#91cc75",
+          "#fac858",
+          "#ee6666",
+          "#73c0de",
+          "#3ba272",
+          "#fc8452",
+          "#9a60b4",
+          "#ea7ccc",
+        ],
+        tooltip: {
+          trigger: "item",
+        },
+        legend: {
+          orient: "vertical",
+          left: "right",
+          itemHeight: 10,
+        },
+        series: [
+          {
+            name: "试题数量",
+            type: "pie",
+            radius: "90%",
+            data,
+            label: { show: false },
+            emphasis: {
+              itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: "rgba(0, 0, 0, 0.5)",
+              },
+            },
+          },
+        ],
+      };
+
+      return option;
+    },
+    async getBlueData() {
+      const res = await courseBlueInfoApi(this.courseId);
+      if (!res.data?.length) return;
+
+      const datas = res.data[0].distributeInfo || [];
+
+      const tableData = [];
+      let questionTypeList = [];
+      const rowspans = [];
+      let curRowIndex = 0;
+      datas.forEach((item) => {
+        const rowCount =
+          item.children && item.children.length ? item.children.length : 1;
+        rowspans.push([curRowIndex, curRowIndex + rowCount - 1]);
+        curRowIndex += rowCount;
+
+        if (item.children && item.children.length) {
+          item.children.forEach((elem) => {
+            const row = {
+              id: `${item.propertyId}_${elem.propertyId}`,
+              firstProperty: item.propertyName,
+              secondProperty: elem.propertyName,
+            };
+
+            if (
+              elem.distributeByQuestionTypeList &&
+              elem.distributeByQuestionTypeList.length
+            ) {
+              elem.distributeByQuestionTypeList.forEach((source) => {
+                row[source.sourceDetailName] = source.questionDifficultInfo
+                  .map(
+                    (item) => `${item.difficultLevel}:${item.questionCount}`
+                  )
+                  .join(",");
+              });
+
+              if (!questionTypeList.length) {
+                questionTypeList = elem.distributeByQuestionTypeList.map(
+                  (source) => source.sourceDetailName
+                );
+              }
+            }
+
+            tableData.push(row);
+          });
+          return;
+        }
+
+        const row = {
+          id: `${item.propertyId}`,
+          firstProperty: item.propertyName,
+          secondProperty: "",
+        };
+
+        if (
+          item.distributeByQuestionTypeList &&
+          item.distributeByQuestionTypeList.length
+        ) {
+          item.distributeByQuestionTypeList.forEach((source) => {
+            row[source.sourceDetailName] = source.questionDifficultInfo
+              .map((item) => `${item.difficultLevel}:${item.questionCount}`)
+              .join(",");
+          });
+
+          if (!questionTypeList.length) {
+            questionTypeList = item.distributeByQuestionTypeList.map(
+              (source) => source.sourceDetailName
+            );
+          }
+        }
+        tableData.push(row);
+      });
+
+      this.questionTypeList = questionTypeList;
+      this.blueData = tableData;
+      this.rowspans = rowspans;
+    },
+    objectSpanMethod({ rowIndex, columnIndex }) {
+      if (columnIndex === 0) {
+        for (let span of this.rowspans) {
+          if (span[0] == rowIndex) {
+            return {
+              rowspan: span[1] - span[0] + 1,
+              colspan: 1,
+            };
+          } else if (span[0] < rowIndex && span[1] >= rowIndex) {
+            return {
+              rowspan: 0,
+              colspan: 0,
+            };
+          }
+        }
+      }
+    },
+  },
+};
+</script>

+ 41 - 1
src/modules/statistics/views/StatisticsManage.vue

@@ -74,6 +74,24 @@
         ></el-table-column>
         <el-table-column prop="paperCount" label="试卷数量" sortable>
         </el-table-column>
+        <el-table-column width="160" label="操作">
+          <template slot-scope="scope">
+            <el-button
+              size="medium"
+              type="text"
+              class="normal"
+              @click="toQuestion(scope.row)"
+              >试题统计
+            </el-button>
+            <el-button
+              size="medium"
+              type="text"
+              class="normal"
+              @click="toPaper(scope.row)"
+              >试卷统计
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table>
       <div class="part-page">
         <el-pagination
@@ -87,6 +105,17 @@
         />
       </div>
     </div>
+
+    <!-- StatisticsQuestionDialog -->
+    <StatisticsQuestionDialog
+      ref="StatisticsQuestionDialog"
+      :course-id="curRow.courseId"
+    />
+    <!-- StatisticsPaperDialog -->
+    <StatisticsPaperDialog
+      ref="StatisticsPaperDialog"
+      :course-id="curRow.courseId"
+    />
   </div>
 </template>
 <script>
@@ -96,10 +125,13 @@ import pickerOptions from "@/constants/datePickerOptions";
 import { omit } from "lodash";
 import { downloadByApi } from "@/plugins/download";
 import { pick } from "lodash";
-// TODO:增加查看每套试卷的题型、难易度、知识点的分布(试卷结构分析)
+
+import StatisticsQuestionDialog from "../components/StatisticsQuestionDialog.vue";
+import StatisticsPaperDialog from "../components/StatisticsPaperDialog.vue";
 
 export default {
   name: "StatisticsManage",
+  components: { StatisticsQuestionDialog, StatisticsPaperDialog },
   computed: {
     queryParams() {
       let startTime = this.searchForm.createTime?.[0] || "";
@@ -191,6 +223,14 @@ export default {
       if (!res) return;
       this.$message.success("导出成功!");
     },
+    toQuestion(row) {
+      this.curRow = row;
+      this.$refs.StatisticsQuestionDialog.open();
+    },
+    toPaper(row) {
+      this.curRow = row;
+      this.$refs.StatisticsPaperDialog.open();
+    },
   },
 };
 </script>

+ 48 - 0
src/plugins/VueCharts.js

@@ -0,0 +1,48 @@
+import Vue from "vue";
+import ECharts from "vue-echarts";
+import { use } from "echarts/core";
+
+// import ECharts modules manually to reduce bundle size
+// 按需模块:https://github.com/apache/echarts/blob/master/src/echarts.all.ts
+// render type
+import { CanvasRenderer, SVGRenderer } from "echarts/renderers";
+
+// charts
+import {
+  BarChart,
+  LineChart,
+  PieChart,
+  ScatterChart,
+  RadarChart,
+  BoxplotChart,
+} from "echarts/charts";
+
+// component
+import {
+  GridComponent,
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent,
+} from "echarts/components";
+
+use([
+  CanvasRenderer,
+  SVGRenderer,
+  BarChart,
+  LineChart,
+  PieChart,
+  ScatterChart,
+  RadarChart,
+  BoxplotChart,
+  GridComponent,
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent,
+]);
+
+// register component to use
+Vue.component("v-chart", ECharts);

+ 32 - 0
yarn.lock

@@ -3281,6 +3281,14 @@ easy-stack@1.0.1:
   resolved "https://registry.npmmirror.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066"
   integrity sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==
 
+echarts@^5.5.1:
+  version "5.5.1"
+  resolved "https://registry.npmmirror.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c"
+  integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==
+  dependencies:
+    tslib "2.3.0"
+    zrender "5.6.0"
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -7197,6 +7205,11 @@ traverse@^0.6.6:
   resolved "https://registry.npmmirror.com/traverse/-/traverse-0.6.8.tgz#5e5e0c41878b57e4b73ad2f3d1e36a715ea4ab15"
   integrity sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==
 
+tslib@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
+  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
 tslib@^2.0.3, tslib@^2.1.0:
   version "2.6.2"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
@@ -7395,6 +7408,18 @@ vue-cli-plugin-element@^1.0.1:
   resolved "https://registry.npmmirror.com/vue-cli-plugin-element/-/vue-cli-plugin-element-1.0.1.tgz#34e58fb65b36cf59afaf14f503288e5e578b1554"
   integrity sha512-OJSOnJtn7f1v/8xX+MJae+RrE8WguhiiG9QTBx/MNOPXYsxqut6Ommo+ZD3raNc7eryhqdM2T/DlMfdvIKpCtw==
 
+vue-demi@^0.13.11:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
+  integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
+
+vue-echarts@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-7.0.3.tgz#bf79f7ee0144bbdc6aee5610e8443fed91f6abbe"
+  integrity sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==
+  dependencies:
+    vue-demi "^0.13.11"
+
 vue-eslint-parser@^8.0.1:
   version "8.3.0"
   resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz#5d31129a1b3dd89c0069ca0a1c88f970c360bd0d"
@@ -7750,3 +7775,10 @@ yorkie@^2.0.0:
     is-ci "^1.0.10"
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
+
+zrender@5.6.0:
+  version "5.6.0"
+  resolved "https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b"
+  integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==
+  dependencies:
+    tslib "2.3.0"