Bladeren bron

feat: Zhurui 完成门户Coder部分

Leone 4 jaren geleden
bovenliggende
commit
20dc6a49d3

+ 6 - 2
package.json

@@ -11,9 +11,12 @@
     "ali-oss": "^6.17.1",
     "axios": "^0.25.0",
     "babel-plugin-import": "^1.13.3",
+    "clipboard": "^2.0.11",
     "core-js": "^3.6.5",
+    "echarts": "^4.8.0",
     "element-plus": "^1.0.2-beta.71",
-    "vue": "^3.0.0",
+    "resize-detector": "^0.3.0",
+    "vue": "^3.1.0",
     "vue-router": "^4.0.12",
     "vuex": "^4.0.2"
   },
@@ -26,11 +29,12 @@
     "@vue/compiler-sfc": "^3.0.0",
     "babel-eslint": "^10.1.0",
     "eslint": "^6.7.2",
-    "webpack": "^4.x",
     "eslint-plugin-vue": "^7.0.0",
     "sass": "^1.49.0",
     "sass-loader": "^7.3.0",
     "terser-webpack-plugin": "^4.2.3",
+    "vue-loader-v16": "^16.0.0-beta.5.4",
+    "webpack": "^4.x",
     "webpackbar": "^5.0.2"
   },
   "eslintConfig": {

+ 77 - 1
src/api/coder.js

@@ -55,6 +55,65 @@ function createCoderExam(data){
   return axios.post(`${prefix}/exam/create`, data);
 }
 
+function deleteCoderExam(examId) {
+  return axios.delete(`${prefix}/exam/${examId}/delete`);
+}
+
+function updateCoderExam(examId, data) {
+  return axios.put(`${prefix}/exam/${examId}/update`, data);
+}
+
+function getCoderExamByExamId(examId) {
+  return axios.get(`${prefix}/exam/${examId}`);
+}
+
+function getCoderProblemById(problemId) {
+  return axios.get(`${prefix}/problem/${problemId}`);
+}
+
+function createCoderProblem(data) {
+  return axios.post(`${prefix}/problem`, data);
+}
+
+function deleteCoderProblem(problemId) {
+  return axios.delete(`${prefix}/problem/${problemId}`);
+}
+
+function updateCoderProblem(problemId, data) {
+  return axios.put(`${prefix}/problem/${problemId}`, data);
+}
+
+function getProblemFile(problemId, filename) {
+  return axios.get(`${prefix}/problem/${problemId}/file`, {
+    params: {
+      "name": encodeURIComponent(filename)
+    }
+  });
+}
+
+function getCoderResultByExamId(examId) {
+  return axios.get(`${prefix}/exam/${examId}/result`);
+}
+
+function getResultByResultId(resultId) {
+  return axios.get(`${prefix}/result/${resultId}`);
+}
+
+function getCommitByCommitId(commitId) {
+  return axios.get(`${prefix}/commit/${commitId}`);
+}
+
+function getRecordByRecordId(recordId) {
+  return axios.get(`${prefix}/record/${recordId}`);
+}
+
+function getExamIsShownForStu(examId) {
+  return axios.get(`${prefix}/exam/${examId}/console`);
+}
+
+function changeExamIsShownForStu(examId, status) {
+  return axios.post(`${prefix}/exam/${examId}/console?showConsole=${status}`);
+}
 
 const coderAPIs = {
   getStudentCoderExams,
@@ -66,7 +125,24 @@ const coderAPIs = {
   getTeacherCoderExamsByCourseId,
   getStudentCoderExamsByCourseId,
   retrievePageProblem,
-  createCoderExam
+
+  createCoderExam,
+  deleteCoderExam,
+  updateCoderExam,
+  getCoderExamByExamId,
+
+  getCoderProblemById,
+  createCoderProblem,
+  deleteCoderProblem,
+  updateCoderProblem,
+  getProblemFile,
+
+  getCoderResultByExamId,
+  getResultByResultId,
+  getCommitByCommitId,
+  getRecordByRecordId,
+  getExamIsShownForStu,
+  changeExamIsShownForStu
 };
 
 export default coderAPIs;

+ 344 - 0
src/components/Echarts.vue

@@ -0,0 +1,344 @@
+<template>
+  <div :style="canvaSize" />
+</template>
+
+<style></style>
+
+<script>
+/* eslint-disable vue/require-default-prop */
+
+import debounce from "lodash.debounce";
+import { addListener, removeListener } from "resize-detector";
+const echarts = require("echarts");
+
+// enumerating ECharts events for now
+// 以下事件可以通过 @[eventName] 进行监听,并且都接受一个参数(触发的事件)
+const EVENTS = [
+  "legendselectchanged",
+  "legendselected",
+  "legendunselected",
+  "legendscroll",
+  "datazoom",
+  "datarangeselected",
+  "timelinechanged",
+  "timelineplaychanged",
+  "restore",
+  "dataviewchanged",
+  "magictypechanged",
+  "geoselectchanged",
+  "geoselected",
+  "geounselected",
+  "pieselectchanged",
+  "pieselected",
+  "pieunselected",
+  "mapselectchanged",
+  "mapselected",
+  "mapunselected",
+  "axisareaselected",
+  "focusnodeadjacency",
+  "unfocusnodeadjacency",
+  "brush",
+  "brushselected",
+  "rendered",
+  "finished",
+  "click",
+  "dblclick",
+  "mouseover",
+  "mouseout",
+  "mousemove",
+  "mousedown",
+  "mouseup",
+  "globalout",
+  "contextmenu"
+];
+
+const ZR_EVENTS = [
+  "click",
+  "mousedown",
+  "mouseup",
+  "mousewheel",
+  "dblclick",
+  "contextmenu"
+];
+
+const INIT_TRIGGERS = ["theme", "initOptions", "autoresize"];
+const REWATCH_TRIGGERS = ["manualUpdate", "watchShallow"];
+
+export default {
+  props: {
+    options: Object,
+    theme: [String, Object],
+    initOptions: Object,
+    group: String,
+    autoresize: Boolean,
+    watchShallow: Boolean,
+    manualUpdate: Boolean,
+    width: {
+      type: Number,
+      default: 600
+    },
+    height: {
+      type: Number,
+      default: 400
+    }
+  },
+  data() {
+    return {
+      lastArea: 0
+    };
+  },
+  computed: {
+    canvaSize() {
+      return { height: `${this.height}px`, width: `${this.width}px` };
+    }
+  },
+  watch: {
+    group(group) {
+      this.chart.group = group;
+    },
+    options: {
+      deep: true,
+      handler() {
+        this.refresh();
+      }
+    }
+  },
+  created() {
+    this.initOptionsWatcher();
+
+    INIT_TRIGGERS.forEach((prop) => {
+      this.$watch(
+          prop,
+          () => {
+            this.refresh();
+          },
+          { deep: true }
+      );
+    });
+
+    REWATCH_TRIGGERS.forEach((prop) => {
+      this.$watch(prop, () => {
+        this.initOptionsWatcher();
+        this.refresh();
+      });
+    });
+  },
+  mounted() {
+    // auto init if `options` is already provided
+    if (this.options) {
+      this.init();
+    }
+  },
+  activated() {
+    if (this.autoresize) {
+      this.chart && this.chart.resize();
+    }
+  },
+  unmounted() {
+    if (this.chart) {
+      this.destroy();
+    }
+  },
+  methods: {
+    // provide an explicit merge option method
+    mergeOptions(options, notMerge, lazyUpdate) {
+      if (this.manualUpdate) {
+        this.manualOptions = options;
+      }
+
+      if (!this.chart) {
+        this.init(options);
+      } else {
+        this.delegateMethod("setOption", options, notMerge, lazyUpdate);
+      }
+    },
+    // just delegates ECharts methods to Vue component
+    // use explicit params to reduce transpiled size for now
+    appendData(params) {
+      this.delegateMethod("appendData", params);
+    },
+    resize(options) {
+      this.delegateMethod("resize", options);
+    },
+    dispatchAction(payload) {
+      this.delegateMethod("dispatchAction", payload);
+    },
+    convertToPixel(finder, value) {
+      return this.delegateMethod("convertToPixel", finder, value);
+    },
+    convertFromPixel(finder, value) {
+      return this.delegateMethod("convertFromPixel", finder, value);
+    },
+    containPixel(finder, value) {
+      return this.delegateMethod("containPixel", finder, value);
+    },
+    showLoading(type, options) {
+      this.delegateMethod("showLoading", type, options);
+    },
+    hideLoading() {
+      this.delegateMethod("hideLoading");
+    },
+    getDataURL(options) {
+      return this.delegateMethod("getDataURL", options);
+    },
+    getConnectedDataURL(options) {
+      return this.delegateMethod("getConnectedDataURL", options);
+    },
+    clear() {
+      this.delegateMethod("clear");
+    },
+    dispose() {
+      this.delegateMethod("dispose");
+    },
+    delegateMethod(name, ...args) {
+      if (!this.chart) {
+        this.init();
+      }
+      return this.chart[name](...args);
+    },
+    delegateGet(methodName) {
+      if (!this.chart) {
+        this.init();
+      }
+      return this.chart[methodName]();
+    },
+    getArea() {
+      return this.$el.offsetWidth * this.$el.offsetHeight;
+    },
+    init(options) {
+      if (this.chart) {
+        return;
+      }
+
+      const chart = echarts.init(this.$el, this.theme, this.initOptions);
+
+      if (this.group) {
+        chart.group = this.group;
+      }
+
+      chart.setOption(options || this.manualOptions || this.options || {}, true);
+
+      // expose ECharts events as custom events
+      EVENTS.forEach((event) => {
+        chart.on(event, (params) => {
+          this.$emit(event, params);
+        });
+      });
+
+      ZR_EVENTS.forEach((event) => {
+        chart.getZr().on(event, (params) => {
+          this.$emit(`zr:${event}`, params);
+        });
+      });
+
+      if (this.autoresize) {
+        this.lastArea = this.getArea();
+        this.__resizeHandler = debounce(
+            () => {
+              if (this.lastArea === 0) {
+                // emulate initial render for initially hidden charts
+                this.mergeOptions({}, true);
+                this.resize();
+                this.mergeOptions(this.options || this.manualOptions || {}, true);
+              } else {
+                this.resize();
+              }
+              this.lastArea = this.getArea();
+            },
+            100,
+            { leading: true }
+        );
+        addListener(this.$el, this.__resizeHandler);
+      }
+
+      Object.defineProperties(this, {
+        // Only recalculated when accessed from JavaScript.
+        // Won"t update DOM on value reset because getters
+        // don"t depend on reactive values
+        width: {
+          configurable: true,
+          getByCommitId: () => {
+            return this.delegateGet("getWidth");
+          }
+        },
+        height: {
+          configurable: true,
+          getByCommitId: () => {
+            return this.delegateGet("getHeight");
+          }
+        },
+        isDisposed: {
+          configurable: true,
+          getByCommitId: () => {
+            return !!this.delegateGet("isDisposed");
+          }
+        },
+        computedOptions: {
+          configurable: true,
+          getByCommitId: () => {
+            return this.delegateGet("getOption");
+          }
+        }
+      });
+
+      this.chart = chart;
+    },
+    initOptionsWatcher() {
+      if (this.__unwatchOptions) {
+        this.__unwatchOptions();
+        this.__unwatchOptions = null;
+      }
+
+      if (!this.manualUpdate) {
+        this.__unwatchOptions = this.$watch(
+            "options",
+            (val, oldVal) => {
+              if (!this.chart && val) {
+                this.init();
+              } else {
+                // mutating `options` will lead to merging
+                // replacing it with new reference will lead to not merging
+                // eg.
+                // `this.options = Object.assign({}, this.options, { ... })`
+                // will trigger `this.chart.setOption(val, true)
+                // `this.options.title.text = "Trends"`
+                // will trigger `this.chart.setOption(val, false)`
+                this.chart.setOption(val, val !== oldVal);
+              }
+            },
+            { deep: !this.watchShallow }
+        );
+      }
+    },
+    destroy() {
+      if (this.autoresize) {
+        removeListener(this.$el, this.__resizeHandler);
+      }
+      this.dispose();
+      this.chart = null;
+    },
+    refresh() {
+      if (this.chart) {
+        this.destroy();
+        this.init();
+      }
+    }
+  },
+  connect(group) {
+    if (typeof group !== "string") {
+      group = group.map((chart) => chart.chart);
+    }
+    echarts.connect(group);
+  },
+  disconnect(group) {
+    echarts.disConnect(group);
+  },
+  registerMap(mapName, geoJSON, specialAreas) {
+    echarts.registerMap(mapName, geoJSON, specialAreas);
+  },
+  registerTheme(name, theme) {
+    echarts.registerTheme(name, theme);
+  },
+  graphic: echarts.graphic
+};
+</script>

+ 31 - 0
src/components/PageHeader.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="page-header">
+    <div class="breadcrumb">
+      <slot name="breadcrumb" />
+    </div>
+    <div class="header-line">
+      <h2 class="title">
+        <slot />
+      </h2>
+      <div class="actions">
+        <slot name="actions" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "PageHeader"
+};
+</script>
+
+<style lang="scss" scoped>
+.page-header {
+  .header-line {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+</style>

+ 40 - 0
src/components/Paper.vue

@@ -0,0 +1,40 @@
+<template>
+  <div
+      class="paper-container"
+      :class="{
+      paper: true,
+      [`vs-shadow-${padding}`]: true,
+      [`vs-padding-${padding}`]: true,
+      [`vs-border-radius-${borderRadius}`]: true
+    }"
+  >
+    <slot />
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Paper",
+  props: {
+    shadow: {
+      type: String,
+      default: 'md'
+    },
+    padding: {
+      type: String,
+      default: 'md'
+    },
+    borderRadius: {
+      type: String,
+      default: 'md'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.paper {
+  overflow: hidden;
+  background-color: white;
+}
+</style>

+ 16 - 0
src/components/SButton.vue

@@ -0,0 +1,16 @@
+<template>
+  <div>
+    <el-button></el-button>
+  </div>
+
+</template>
+
+<script>
+export default {
+  name: "SButton"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 8 - 3
src/components/TestCard.vue

@@ -157,6 +157,7 @@ export default {
       const m = (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes());
       return Y + M + D + h + m;
     },
+    // TODO 分情况处理跳转
     async toTestInfo() {
       if (!this.isLogin) {
         ElMessage({
@@ -169,11 +170,15 @@ export default {
         return;
       }
 
+      console.log(this.testVO);
+
       if (this.isCreator) {
         if (this.type === "eval") {
-          window.open("https://eval.seec.seecoder.cn/teacher/exam/" + this.testVO.examId + "/details?id_token" + this.$store.state.user.token);
+          this.$router.push(`/exam/${this.testVO.examId}`);
+          // window.open("https://eval.seec.seecoder.cn/teacher/exam/" + this.testVO.examId + "/details?id_token" + this.$store.state.user.token);
         } else {
-          window.open("https://coder.seec.seecoder.cn/exam/" + this.testVO.id + "?id_token=" + this.$store.state.user.token);
+          this.$router.push(`/exam/${this.testVO.id}`); //跳转到考试详情页
+          // window.open("https://coder.seec.seecoder.cn/exam/" + this.testVO.id + "?id_token=" + this.$store.state.user.token);
         }
       } else if (this.isTeacher) {
         this.$message.error("无权访问其他教师创建的课程");
@@ -302,4 +307,4 @@ export default {
   font-size: 10px;
   margin-bottom: 16px;
 }
-</style>
+</style>

+ 36 - 1
src/router/index.js

@@ -62,7 +62,7 @@ const routes = [
   },
 
   {
-    path: "/problem/:id",
+    path: "/problem/:problemId",
     name: "ProblemDetail",
     component: () => import("../views/ProblemPage/ProblemDetail.vue"),
   },
@@ -76,6 +76,41 @@ const routes = [
     name: "ProblemUpload",
     component: () => import("../views/ProblemPage/ProblemUpload.vue"),
   },
+  {
+    path: "/exam/:examId",
+    name: "ExamDetail",
+    component: () => import("../views/ExamPage/ExamDetail.vue")
+  },
+  {
+    path: "/exam/:examId/commit/:commitId",
+    name: "CommitDetail-First",
+    component: () => import("../views/ExamPage/ResultPage/CommitPage/CommitDetail")
+  },
+  {
+    path: "/exam/:examId/commit/:commitId/record/:recordId",
+    name: "RecordDetail-First",
+    component: () => import("../views/ExamPage/ResultPage/CommitPage/RecordPage/RecordDetail")
+  },
+  {
+    path: "/exam/:examId/result",
+    name: "ResultPage",
+    component: () => import("../views/ExamPage/ResultPage/ResultPage")
+  },
+  {
+    path: "/exam/:examId/result/:resultId",
+    name: "ResultDetail",
+    component: () => import("../views/ExamPage/ResultPage/ResultDetail")
+  },
+  {
+    path: "/exam/:examId/result/:resultId/commit/:commitId",
+    name: "CommitDetail-Second",
+    component: () => import("../views/ExamPage/ResultPage/CommitPage/CommitDetail")
+  },
+  {
+    path: "/exam/:examId/result/:resultId/commit/:commitId/record/:recordId",
+    name: "RecordDetail-Second",
+    component: () => import("../views/ExamPage/ResultPage/CommitPage/RecordPage/RecordDetail")
+  }
 ];
 
 const router = createRouter({

+ 17 - 2
src/store/modules/user.js

@@ -133,8 +133,23 @@ const user = {
     },
     isLogin(state) {
       return state.token !== undefined;
-    }
+    },
+    isStudentGroup: (state) => {
+      return state.role === "STUDENT" ||
+          state.role === "STAFF" ||
+          state.role === "ASSISTANT" ||
+          state.role === "SEEC" ||
+          state.role === "SEEC";
+    },
+    isTeacherGroup: (state) => {
+      return state.role === "TEACHER" || state.role === "HR";
+    },
+    isAssistantGroup: (state) =>
+        state.role === "ASSISTANT" ||
+        state.role === "SEEC" ||
+        state.role === "SEECX",
+    isAdmin: (state) => state.id === 0, // TODO: bad design,
   }
 };
 
-export default user;
+export default user;

+ 65 - 0
src/utils/coder-utils.js

@@ -63,3 +63,68 @@ export function problemTypeToTagContent(type) {
     return type;
 }
 
+export function examStatusToTagType(status) {
+    switch (status) {
+        case "READY":
+            return "";
+        case "AVAILABLE":
+            return "success";
+        case "FINISHED":
+            return "warning";
+        case "UNAVAILABLE":
+            return "danger";
+    }
+    return "info";
+}
+
+export function examStatusToTagContent(status) {
+    switch (status) {
+        case "PREPARING":
+            return "准备中";
+        case "UNAVAILABLE":
+            return "初始化失败";
+        case "READY":
+            return "尚未开始";
+        case "AVAILABLE":
+            return "正在进行";
+        case "FINISHED":
+            return "已结束";
+        case "CLOSED":
+            return "已关闭";
+    }
+    return "未知状态";
+}
+
+export function buildStatusToTagType(status) {
+    switch (status) {
+        case "RUNNING":
+            return "";
+        case "SUCCESS":
+            return "success";
+        case "UNSTABLE":
+            return "warning";
+        case "FAILURE":
+        case "TIMEOUT":
+            return "danger";
+    }
+    return "info";
+}
+
+export function buildStatusToTagContent(status) {
+    switch (status) {
+        case "SUCCESS":
+            return "所有用例通过";
+        case "UNSTABLE":
+            return "有用例未通过";
+        case "FAILURE":
+            return "编译或运行失败";
+        case "TIMEOUT":
+            return "运行超时";
+        case "WAITING":
+            return "等待运行";
+        case "RUNNING":
+            return "正在运行";
+    }
+    return "未知状态";
+}
+

+ 279 - 13
src/views/CoursePage/CourseDetail.vue

@@ -407,6 +407,119 @@
         <el-button plain type="primary" @click="handleAddMemberConfirm">确认</el-button>
       </div>
     </el-dialog>
+    <!-- 创建Coder考试-->
+    <el-dialog v-model="coderExamDialog" :before-close="handleExamFormCancel" :width="1200" :title="coderExamForm.dialogTitle">
+      <el-form ref="coderExamFormRef" :model="coderExamForm" :rules="coderExamFormRules" label-width="150px">
+        <el-form-item label="所属课程" prop="course">
+          <el-input disabled :placeholder="courseName" />
+        </el-form-item>
+        <el-form-item label="测试名称" prop="name">
+          <el-input class="course-form-item" v-model="coderExamForm.name" placeholder="请输入课程名称"></el-input>
+        </el-form-item>
+        <el-form-item label="测试时间" prop="range">
+          <el-date-picker v-model="coderExamForm.range" range-separator="至"
+                          start-placeholder="开始时间" end-placeholder="结束时间" type="datetimerange">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="允许学生查看控制台">
+          <el-switch v-model="coderExamForm.isConsoleOutputShownForStu" :disabled="switchExamDisabled" active-text="开启" inactive-text="不开启"/>
+        </el-form-item>
+        <el-form-item label="启用代码分析">
+          <el-switch v-model="coderExamForm.isCodeAnalysisNeeded" :disabled="switchExamDisabled" active-text="开启" inactive-text="不开启"/>
+        </el-form-item>
+        <el-form-item label="题目列表">
+          <el-button type="text" @click="showProblemDialog">选择题目</el-button>
+        </el-form-item>
+        <el-form-item v-if="$store.state.problem.cart.length">
+          <el-collapse accordion>
+            <el-collapse-item
+                v-for="problem in $store.state.problem.cart"
+                :key="problem.id"
+                :name="problem.id"
+            >
+              <template #title>
+                <span>{{ problem.name }}</span>
+                <el-tag class="problem-type">{{ problem.type }}</el-tag>
+              </template>
+              <b>题目描述:</b>
+              <span class="problem-description">{{ problem.description }}</span>
+              <div class="remove-button">
+                <el-button
+                    type="danger"
+                    size="mini"
+                    @click="$store.commit('removeProblem', problem)"
+                >移除该题</el-button
+                >
+              </div>
+            </el-collapse-item>
+          </el-collapse>
+        </el-form-item>
+      </el-form>
+      <div style="margin: auto;width: fit-content">
+        <el-button type="primary" @click="handleExamCreate">确定</el-button>
+        <el-button @click="handleExamFormCancel">取消</el-button>
+      </div>
+    </el-dialog>
+    <!-- 展示Coder所有题目-->
+    <el-dialog v-model="coderProblemDialog" :width="1200">
+      <el-input v-model="dialogProblemName"
+                style="width:200px;"
+                placeholder="搜索"
+      />
+      <el-select v-model="dialogProblemTypeStr" style="width: 300px" @change="fetchProblemData(1)">
+        <el-option v-for="item in problemTypeOptions" :key="item.value" :value="item.value">
+          {{item.label}}
+        </el-option>
+      </el-select>
+      <el-table :data="problemList" >
+        <el-table-column type="expand">
+          <template #default="props">
+            <div>{{ props.row.description }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="名称"></el-table-column>
+        <el-table-column label="类型">
+          <template #default="scope">
+            <span>{{ problemTypeToTagContent(scope.row.type) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="上传时间">
+          <template #default="scope">
+            <span>{{ formatTimestamp(scope.row.createdAt)}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200">
+          <template #default="scope">
+            <el-button
+                type="primary"
+                size="mini"
+                @click="checkDetails"
+            >详情</el-button
+            >
+            <el-button
+                v-if="removeEnable(scope.row)"
+                type="danger"
+                size="mini"
+                @click="$store.commit('removeProblem', scope.row)"
+            >移出试题清单</el-button
+            >
+            <el-button
+                v-else-if="addEnable(scope.row)"
+                type="success"
+                size="mini"
+                @click="$store.commit('addProblem', scope.row)"
+            >加入试题清单</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="problemPageInfo.total"
+          @current-change="changePage"
+      />
+    </el-dialog>
   </div>
 </template>
 
@@ -416,6 +529,7 @@ import FileEntry from "./components/FileEntry";
 import {ElMessageBox, ElMessage} from "element-plus";
 import variables from "@/utils/variables";
 import ProjectLongCard from "@/components/project/ProjectLongCard";
+import {formatTimestamp, problemTypeOptions, problemTypeToTagContent} from "@/utils/coder-utils";
 
 export default {
   name: "CourseDetail",
@@ -543,6 +657,43 @@ export default {
         dialogTitle: "组员信息",
         phone: null,
       },
+
+      //create Coder exam
+      coderExamDialog: false,
+      coderExamDialogTitle: "新建Coder考试",
+      coderExamForm: {
+        examType: "coder",
+        course: "",
+        name: "",
+        range: [],
+        isCodeAnalysisNeeded: false,
+        isConsoleOutputShownForStu: false
+      },
+      coderExamFormRules: {
+        // type: {
+        //   required: true, message: "请选择测试种类",
+        // },
+        name: {
+          required: true, message: "请输入测试名称", trigger: "blur",
+        },
+        course: {
+          required: true, message: "请选择测试名称",
+        },
+        range: {
+          required: true, message: "请选择测试时间段", trigger: "blur",
+        }
+      },
+      switchExamDisabled: false,
+
+      //Coder problems
+      coderProblemDialog: false,
+      dialogProblemPage: 1,
+      dialogProblemLimit: 10,
+      dialogProblemTypeStr: "",
+      dialogProblemName: "",
+      problemList: [],
+      problemPageInfo: {}
+
     };
   },
   computed: {
@@ -592,6 +743,9 @@ export default {
         return senderId === Number(_this.$store.state.user.id);
       };
     },
+    problemTypeOptions() {
+      return problemTypeOptions(true);
+    }
   },
   mounted() {
     switch (this.$route.params.jump_from_message) {
@@ -602,19 +756,25 @@ export default {
         this.tabName = "comment";
         break;
     }
-    this.$apis.getCourseById(this.$route.params.id)
-      .then(res => {
-        this.courseInfo = res.data.data;
-        this.$apis.checkCourseSelection(this.$route.params.id, this.$store.state.user.id)
-          .then(res => {
-            this.isCourseSelected = res.data.data;
-            if (this.isCreator || this.isCourseSelected) {
-              this.switchTab(this.tabName);
-            }
-          });
-      });
+    this.fetchData();
   },
   methods: {
+    fetchData() {
+      this.$apis.getCourseById(this.$route.params.id)
+          .then(res => {
+            this.courseInfo = res.data.data;
+            this.coderExamForm.course = this.courseInfo.coderId; //当前课程的coderId固定
+            this.$apis.checkCourseSelection(this.$route.params.id, this.$store.state.user.id)
+                .then(res => {
+                  this.isCourseSelected = res.data.data;
+                  if (this.isCreator || this.isCourseSelected) {
+                    this.switchTab(this.tabName);
+                  }
+                });
+          });
+    },
+    formatTimestamp,
+    problemTypeToTagContent,
     // course
 
     handleSelectCourse() {
@@ -783,7 +943,8 @@ export default {
           if (this.courseInfo.coderId === null) {
             ElMessage.error("本课程没有链接coder系统");
           }
-          window.open("https://coder.seec.seecoder.cn/exam/create/?id_token=" + this.$store.state.user.token);
+          this.coderExamDialog = true;
+          // window.open("https://coder.seec.seecoder.cn/exam/create/?id_token=" + this.$store.state.user.token);
           break;
       }
     },
@@ -821,7 +982,11 @@ export default {
         coderUrl = "https://coder.seec.seecoder.cn/exam/";
         url = type === "eval" ? evalUrl + task["examId"] + "/details" : coderUrl + task.id;
       }
-      window.open(url + "?id_token=" + this.$store.state.user.token);
+
+      if (type === "coder") {
+        this.$router.push(`/exam/${task.id}`);
+      }
+      // window.open(url + "?id_token=" + this.$store.state.user.token);
     },
 
     // announcement
@@ -1196,6 +1361,107 @@ export default {
       });
 
     },
+
+    // 创建Coder考试的Dialog
+    handleExamFormCancel(done) {
+      //关闭dialog的提示
+      ElMessageBox.confirm("是否确认退出?退出后将不保存表格信息。", "提示", {
+        confirmButtonText: "确认退出",
+        cancelButtonText: "我再想想",
+        type: "warning",
+        center: true,
+      })
+          .then(() => {
+            this.coderExamDialog = false;
+            // this.clearForm();
+            //TODO 清除表格信息
+            done();
+          })
+          .catch((err) => {
+            console.log(err);
+          });
+    },
+
+    // 展示Coder所有的problem题目(代码题)
+    showProblemDialog(){ //打开弹框,获取初始数据
+      this.fetchProblemData();
+      this.coderProblemDialog = true;
+    },
+
+    fetchProblemData(page=1) { // 按需获得Coder题目
+      this.$apis.retrievePageProblem(page, this.dialogProblemLimit, this.dialogProblemTypeStr, this.dialogProblemName)
+          .then(_res => {
+            this.problemPageInfo = _res.data.page;
+            this.problemList = _res.data.res;
+          }).catch(err => {
+        console.log(err);
+      });
+    },
+
+    //验证表单数据,创建考试
+    handleExamCreate(){
+      const form = this.$refs.coderExamFormRef;
+      form.validate((valid) => {
+        if (valid) {
+          console.log("validate 成功");
+          this.createCoderExam();
+        } else {
+          console.log("validate 失败了");
+          return false;
+        }
+      });
+    },
+
+    createCoderExam(){
+      //创建Coder考试
+      this.$apis.createCoderExam({
+        course: this.coderExamForm.course,
+        name: this.coderExamForm.name,
+        startAt: this.coderExamForm.range[0].getTime()/1000,
+        endAt: this.coderExamForm.range[1].getTime()/1000,
+        problems: this.$store.state.problem.cart.map((item) => item.id),
+        isConsoleOutputShownForStu: this.coderExamForm.isConsoleOutputShownForStu
+      }).then(_res => {
+        this.$store.commit("clearProblem");
+        ElMessage.success("创建测试成功!");
+        this.clearCoderExamForm(); //清空Coder考试表单内容
+        this.fetchData(); //重新获取数据,实时刷新页面
+        this.coderExamDialog = false;
+      }).catch(_err => {
+        console.log(_err);
+        ElMessage.warning("创建测试失败!");
+      });
+    },
+
+    //查看题目详情(暂不支持)
+    checkDetails(){
+      ElMessage.warning("暂不支持查看详情!");
+    },
+
+    //题目购物车cart
+    removeEnable(problem) { //是否可以移除题目
+      return this.$store.state.problem.cart.length && this.$store.state.problem.cart
+          .map((item) => item.id)
+          .includes(problem.id);
+    },
+    addEnable(problem) { //是否可以添加题目
+      return this.$store.getters.isTeacher && problem.modifiedAt;
+    },
+
+    changePage(val){
+      this.fetchProblemData(val);
+    },
+
+    clearCoderExamForm(){
+      this.coderExamForm = {
+        examType: "coder",
+        course: this.courseInfo.coderId,
+        name: "",
+        range: [],
+        isCodeAnalysisNeeded: false,
+        isConsoleOutputShownForStu: false
+      };
+    },
   },
 };
 </script>

+ 470 - 0
src/views/ExamPage/ExamDetail.vue

@@ -0,0 +1,470 @@
+<template>
+  <div class="exam-container">
+    <el-card>
+      <template #header>
+        <page-header>
+          <template #actions>
+            <el-button @click="fetchExamDetail" :loading="loading.refresh">
+              <el-icon><RefreshRight /></el-icon>
+            </el-button>
+            <el-button
+                v-if="(isStudentGroup || isTeacherGroup)"
+                @click="jumpToCoderOnlineExam"
+            >
+              在线自测
+            </el-button>
+            <el-button
+                v-if="isTeacherGroup || isAdmin || isAssistantGroup"
+                @click="checkStudentGrade"
+            >
+              查看学生成绩
+            </el-button>
+            <el-button
+                v-if="isTeacherGroup || isAdmin"
+                disabled
+                @click="goToDetect()"
+            >
+              查看查重结果
+            </el-button>
+            <el-button
+                v-if="isTeacherGroup || isAdmin"
+                variant="danger"
+                @click="dialog.delete = true"
+            >
+              删除考试
+            </el-button>
+          </template>
+          {{ exam.name }}
+        </page-header>
+      </template>
+
+      <paper>
+        <el-descriptions>
+          <el-descriptions-item
+              label="编号:"
+              label-class-name="label-bold"
+          >
+            {{ exam.id }}
+          </el-descriptions-item>
+          <el-descriptions-item
+              label="创建者:"
+              label-class-name="label-bold"
+          >
+            {{ exam.creator ? exam.creator.username : '' }}
+          </el-descriptions-item>
+          <el-descriptions-item
+              label="创建时间:"
+              label-class-name="label-bold"
+          >
+            {{ formatTimestamp(exam.created_at) }}
+          </el-descriptions-item>
+          <el-descriptions-item
+              v-if="(isStudentGroup || isTeacherGroup) && exam.joined"
+              :label="`${isTeacherGroup ? '自测' : ''}成绩`"
+              label-class-name="label-bold"
+          >
+            {{ result.score }}
+          </el-descriptions-item>
+          <el-descriptions-item
+              label="状态:"
+              label-class-name="label-bold"
+          >
+            <el-tag size="mini" :type="statusTagType">
+              {{ statusTagContent }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item
+              label="起止时间:"
+              :span="2"
+              label-class-name="label-bold"
+          >
+            {{ formatTimestamp(exam.startAt) }} 至 {{ formatTimestamp(exam.endAt) }}
+            <a
+                v-if="isTeacherGroup || isAdmin"
+                class="changeable"
+                @click="dialog.update = true"
+            >
+              修改
+            </a>
+          </el-descriptions-item>
+          <el-descriptions-item
+              label="试卷地址:"
+              :span="3"
+              label-class-name="label-bold"
+          >
+          <span id="exam-url">
+          {{ exam.url ? exam.url : '不可用' }}
+        </span>
+            <a
+                v-if="exam.url"
+                id="copy-btn"
+                class="changeable"
+                data-clipboard-target="#exam-url"
+                @click="copyAddress"
+            >
+              复制
+            </a>
+          </el-descriptions-item>
+          <el-descriptions-item
+              v-if="isTeacherGroup || isAdmin"
+              label="是否允许学生查看控制台:"
+              :span="2"
+              label-class-name="label-bold"
+          >
+            <span>{{ isConsoleOutputShownForStu ? "允许" : "不允许" }}</span>
+            <a class="changeable" @click="dialog.console = true">
+              修改
+            </a>
+          </el-descriptions-item>
+        </el-descriptions>
+      </paper>
+
+      <el-tabs
+          class="top-divide exam-detail-tabs"
+          v-model="tabValue"
+      >
+        <el-tab-pane v-if="exam.joined" label="提交记录" name="commits">
+          <el-table v-if="exam.joined" :data="result.commits">
+            <el-table-column prop="id" label="#" width="80" />
+            <el-table-column label="提交时间">
+              <template #default="scope">
+                <span>{{ formatTimestamp(scope.row.created_at) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="状态" width="180">
+              <template #default="scope">
+              <span>
+                {{ scope.row.finished ? '运行结束' : '等待或运行中' }}
+              </span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="score" label="成绩" width="120" />
+            <el-table-column label="操作" width="120">
+              <template #default="scope">
+                <el-button
+                    type="primary"
+                    size="mini"
+                    @click="
+                    $router.push(
+                    {
+                      path: `/exam/${$route.params.examId}/commit/${scope.row.id}`
+                    })
+                "
+                >
+                  详情
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane
+            v-if="exam.problems && exam.problems.length"
+            class="problems-tab-pane"
+            label="题目列表"
+            name="problems"
+        >
+          <el-collapse accordion>
+            <el-collapse-item
+                v-for="problem in exam.problems"
+                :key="problem.id"
+                :name="problem.id"
+            >
+              <template #title>
+                {{ problem.name }}
+                <el-tag class="problem-tag">
+                  {{ problemTypeToTagContent(problem.type) }}
+                </el-tag>
+              </template>
+              <div style="display: inline;margin-right: 40px">{{ problem.description }}</div>
+              <el-button
+                  v-if="isTeacherGroup || isAdmin"
+                  @click="jumpToProblemDetail(problem.id)"
+                  style="float: right;margin-right: 40px"
+                  size="small"
+              >预览题目</el-button>
+            </el-collapse-item>
+          </el-collapse>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+    <el-dialog title="确认删除考试吗?" v-model="dialog.delete">
+      <el-alert
+          style="margin-bottom: 20px"
+          title="注意:所有与该场考试有关的提交数据都将被删除!"
+          type="error"
+          :closable="false"
+      ></el-alert>
+      <el-descriptions>
+        <el-descriptions-item label="考试ID:">{{ exam.id }}</el-descriptions-item>
+        <el-descriptions-item label="考试名称:">{{ exam.name }}</el-descriptions-item>
+      </el-descriptions>
+
+      <template #footer class="dialog-footer">
+        <el-button @click="dialog.delete = false">取消</el-button>
+        <el-button
+            type="danger"
+            :loading="loading.delete"
+            @click="deleteExam"
+        >确定</el-button>
+      </template>
+    </el-dialog>
+    <el-dialog title="修改起止时间" v-model="dialog.update" width="960px">
+      <el-descriptions>
+        <el-descriptions-item label="原起止时间:" :span="2">
+          {{ formatTimestamp(exam.startAt) }} 至 {{ formatTimestamp(exam.endAt) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="新起止时间:" :span="2">
+          <el-date-picker
+              v-model="range"
+              type="datetimerange"
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+          />
+        </el-descriptions-item>
+      </el-descriptions>
+      <el-alert title="注意" type="warning" style="margin-top: 20px">
+        <p>开始时间早于当前时间考试将立即开始</p>
+        <p>开始时间晚于当前时间考试将回归未开始状态</p>
+        <p>结束时间早于当前时间考试将立即结束并关闭</p>
+        <p>考试关闭后将无法恢复</p>
+      </el-alert>
+
+      <template #footer class="dialog-footer">
+        <el-button @click="dialog.update = false">取消</el-button>
+        <el-button
+            variant="primary"
+            :loading="loading.update"
+            :disabled="range.length !== 2"
+            @click="updateExam"
+        >
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+    <el-dialog title="修改学生查看控制台权限" v-model="dialog.console">
+      <el-descriptions>
+        <el-descriptions-item label="考试ID:">{{ exam.id }}</el-descriptions-item>
+        <el-descriptions-item label="考试名称:">{{ exam.name }}</el-descriptions-item>
+        <el-descriptions-item label="当前控制台权限:">{{isConsoleOutputShownForStu?"":"不"}}允许学生查看控制台输出</el-descriptions-item>
+      </el-descriptions>
+      <template #footer class="dialog-footer">
+        <el-button @click="dialog.console = false">取消</el-button>
+        <el-button
+            variant="primary"
+            :loading="loading.console"
+            @click="changeShowConsoleForStu"
+        >
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {examStatusToTagContent, examStatusToTagType, formatTimestamp, problemTypeToTagContent} from "@/utils/coder-utils";
+import {mapGetters} from "vuex";
+import Paper from "@/views/ProblemPage/components/Paper";
+import PageHeader from "../../components/PageHeader";
+import {ElMessage} from "element-plus";
+import Clipboard from "clipboard";
+
+export default {
+  name: "ExamDetail",
+  components: {
+    Paper,
+    PageHeader,
+  },
+  props: {
+  },
+  data() {
+    return {
+      exam: { joined: true },
+      result: {},
+      isConsoleOutputShownForStu: false,
+
+      dialog: {
+        update: false,
+        delete: false,
+        console: false
+      },
+
+      loading: {
+        join: false,
+        update: false,
+        delete: false,
+        console: false,
+        refresh: false
+      },
+
+      range: [],
+    };
+  },
+  computed: {
+    ...mapGetters([
+      "isStudentGroup",
+      "isTeacherGroup",
+      "isAdmin",
+      "isAssistantGroup"
+    ]),
+    statusTagType(){
+      return examStatusToTagType(this.exam.status);
+    },
+    statusTagContent(){
+      return examStatusToTagContent(this.exam.status);
+    },
+    joinDisabled() {
+      return (
+          this.exam.status === "UNAVAILABLE" ||
+          this.exam.status === "FINISHED" ||
+          this.exam.status === "CLOSED"
+      );
+    },
+    tabValue() {
+      return this.exam.joined? "commits": "problems";
+    }
+  },
+  mounted() {
+    this.fetchExamDetail();
+  },
+  methods: {
+    /**
+     * 获取当前考试信息
+     */
+    fetchExamDetail() {
+      this.loading.refresh = true;
+      const { examId } = this.$route.params;
+      this.$apis.getCoderExamByExamId(examId)
+          .then(_res => {
+            this.exam = _res.data.res;
+            if (!this.isAdmin && this.exam.joined) {
+              this.fetchResult();
+            }
+          }).catch(_err => {
+            ElMessage.error("当前考试不存在!跳转到管理页面!");
+            this.$router.push("/manage");
+      });
+      if (this.isTeacherGroup) {
+        this.$apis.getExamIsShownForStu(examId)
+            .then(_res => {
+              this.isConsoleOutputShownForStu = _res.data.res ?? false;
+              this.loading.refresh = false;
+            }).catch(_err => {
+              ElMessage.error(_err);
+        });
+      }
+    },
+    fetchResult() {
+      this.$apis.getResultByResultId(this.exam.result?.id)
+          .then(_res => {
+            this.result = _res.data.res ?? {};
+          }).catch(_err => {
+            ElMessage.error(_err);
+      });
+    },
+    formatTimestamp,
+    problemTypeToTagContent,
+    jumpToCoderOnlineExam() {
+      ElMessage.info("跳转到coder进行在线考试");
+      let nextURL = "https://coder.seec.seecoder.cn/exam/" + this.exam.id + "?id_token=" + this.$store.state.user.token;
+      window.open(nextURL);
+      //TODO 在Coder中设置路由拦截器,进行token解析
+    },
+    goToDetect() {
+      // ElMessage.error("暂不支持查看查重结果");
+    },
+    checkStudentGrade() {
+      this.$router.push(this.$route.path+"/result");
+    },
+    deleteExam() {
+      this.loading.delete = true;
+      const { examId } = this.$route.params;
+      this.$apis.deleteCoderExam(examId)
+          .then(_res => {
+            ElMessage.success("删除考试成功");
+            this.dialog.delete = false;
+            this.$router.push("/manage");
+          }).catch(_err => {
+        ElMessage.error("删除考试失败");
+      }).finally(() => {
+        this.loading.delete = false;
+      });
+    },
+    updateExam() {
+      this.loading.update = true;
+      const { examId } = this.$route.params;
+
+      this.$apis.updateCoderExam(examId, {
+        course: this.exam.courseId,
+        startAt: this.range[0].getTime() / 1000,
+        endAt: this.range[1].getTime() / 1000
+      }).then(_res => {
+        console.log(_res);
+        this.exam = _res.data.res ?? {};
+        ElMessage.success({
+          message: "更新考试时间段成功!",
+          center: true
+        });
+        this.dialog.update = false;
+      }).catch(_err => {
+        ElMessage.error({
+          message: "更新考试时间段失败!",
+          center: true
+        });
+      }).finally(() => {
+        this.loading.update = false;
+        this.range = [];
+      });
+    },
+    copyAddress() {
+      const cb = new Clipboard("#copy-btn");
+      cb.on("success", (e) => {
+        ElMessage.success({
+          message: "已复制到剪切板!",
+          center: true
+        });
+        e.clearSelection();
+      });
+      cb.on("error", (e) => {
+        ElMessage.error({
+          message: "浏览器不支持复制到剪贴板!",
+          center: true
+        });
+        e.clearSelection();
+      });
+    },
+    changeShowConsoleForStu() {
+      this.loading.console = true;
+      const { examId } = this.$route.params;
+      const showConsoleForStu = !this.isConsoleOutputShownForStu;
+
+      this.$apis.changeExamIsShownForStu(examId, showConsoleForStu)
+          .then(_res => {
+            this.isConsoleOutputShownForStu = _res.data.res ?? false;
+            ElMessage.success("修改控制台权限成功!");
+          }).catch(_err => {
+        console.log(_err);
+        ElMessage.success("修改控制台权限失败!");
+      }).finally(() => {
+        this.dialog.console = false;
+      });
+    },
+    jumpToProblemDetail(problemId) {
+      this.$router.push("/problem/"+problemId);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.exam-container {
+  margin: 40px 10vw 80px 10vw;
+  min-height: calc(100vh - 100px);
+}
+.changeable {
+  color: blue;
+  cursor: pointer;
+}
+
+</style>

+ 46 - 0
src/views/ExamPage/ResultPage/CommitPage/CommitDetail.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="commit-container">
+    <el-card>
+      <template #header>
+        <page-header>
+          提交详情
+        </page-header>
+      </template>
+      <CommitBoard :commit-id="commitId"></CommitBoard>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import CommitBoard from "@/views/ExamPage/ResultPage/CommitPage/components/CommitBoard";
+import PageHeader from "@/components/PageHeader";
+export default {
+  name: "CommitDetail",
+  components: {PageHeader, CommitBoard},
+  data() {
+    return {
+
+    };
+  },
+  computed: {
+    commitId() {
+      const {commitId} = this.$route.params;
+      return commitId;
+    }
+  },
+  mounted() {
+
+  },
+  methods: {
+
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.commit-container {
+  margin: 40px 10vw 80px 10vw;
+  min-height: calc(100vh - 100px);
+}
+
+</style>

+ 155 - 0
src/views/ExamPage/ResultPage/CommitPage/RecordPage/RecordDetail.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="record-container">
+    <el-card>
+      <template #header>
+        <page-header>
+          <template #actions>
+            <el-button disabled>
+              查看调用图
+            </el-button>
+            <el-button disabled>
+              查看代码度量
+            </el-button>
+            <el-button disabled>
+              查看质量分析
+            </el-button>
+            <el-button
+                v-if="
+            isTeacherGroup ||
+              isAdmin ||
+              isConsoleOutputShownForStu ||
+              isAssistantGroup
+          "
+                @click="dialog = true"
+            >
+              查看控制台输出
+            </el-button>
+          </template>
+          运行详情
+        </page-header>
+      </template>
+      <paper style="padding: 20px">
+        <el-descriptions>
+          <el-descriptions-item label="状态:">
+            <el-tag :type="statusTagType"> {{ statusTagContent }} </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="错误用例数:">
+            {{ record.failures ? record.failures.length : 0 }}
+          </el-descriptions-item>
+          <el-descriptions-item label="分数:">
+            {{ record.score }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </paper>
+      <div class="top-divide fill-card">
+        <el-table :data="record.failures" class="failure-table">
+          <el-table-column
+              v-if="isTeacherGroup || isAdmin || isAssistantGroup"
+              type="expand"
+          >
+            <template #default="scope">
+              <pre><code>{{ scope.row.trace }}</code></pre>
+            </template>
+          </el-table-column>
+          <el-table-column label="错误用例" prop="name" />
+        </el-table>
+      </div>
+
+    </el-card>
+
+    <el-dialog
+        v-model="dialog"
+        title="控制台输出"
+        width="1080px"
+        class="console-output-dialog"
+    >
+      <pre><code class="console-output-code"> {{record.consoleOutput}} </code></pre>
+    </el-dialog>
+  </div>
+
+</template>
+
+<script>
+import PageHeader from "@/components/PageHeader";
+import {mapGetters} from "vuex";
+import {ElMessage} from "element-plus";
+import Paper from "@/views/ProblemPage/components/Paper";
+import {buildStatusToTagContent, buildStatusToTagType} from "@/utils/coder-utils";
+export default {
+  name: "RecordDetail",
+  components: {Paper, PageHeader},
+  props: {
+
+  },
+  data() {
+    return {
+      record: {},
+      dialog: false,
+      isConsoleOutputShownForStu: false,
+    };
+  },
+  computed: {
+    ...mapGetters([
+        "isTeacherGroup",
+        "isAdmin",
+        "isAssistantGroup"
+    ]),
+    statusTagType(){
+      return buildStatusToTagType(this.record.status);
+    },
+    statusTagContent(){
+      return buildStatusToTagContent(this.record.status);
+    },
+  },
+  mounted() {
+    this.fetchData();
+    this.checkConsole();
+  },
+  methods: {
+    fetchData() {
+      const {recordId} = this.$route.params;
+      this.$apis.getRecordByRecordId(recordId)
+          .then(_res => {
+            this.record = _res.data.res;
+          }).catch(_err => {
+            ElMessage.error(_err);
+      });
+    },
+    checkConsole() {
+      const {examId} = this.$route.params;
+      this.$apis.getExamIsShownForStu(examId)
+          .then(_res => {
+            this.isConsoleOutputShownForStu = _res.data?.res;
+          }).catch(_err => {
+            ElMessage.error(_err);
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+pre code {
+  font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
+  overflow: auto;
+  display: block;
+  padding: 10px;
+  line-height: 1.428571429;
+  color: #333;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #e5e5e5;
+  border: 1px solid #ccc;
+  border-radius: 0.5em;
+  white-space: pre;
+}
+.console-output-code {
+  height: 50vh;
+  min-height: 480px;
+}
+
+.record-container {
+  margin: 40px 10vw 80px 10vw;
+  min-height: calc(100vh - 100px);
+}
+</style>

+ 13 - 0
src/views/ExamPage/ResultPage/CommitPage/RecordPage/components/RecordBoard.vue

@@ -0,0 +1,13 @@
+<template>
+
+</template>
+
+<script>
+export default {
+  name: "RecordBoard"
+}
+</script>
+
+<style scoped>
+
+</style>

+ 101 - 0
src/views/ExamPage/ResultPage/CommitPage/components/CommitBoard.vue

@@ -0,0 +1,101 @@
+<template>
+  <paper>
+    <el-descriptions>
+      <el-descriptions-item label="编号:">{{ commit.id }}</el-descriptions-item>
+      <el-descriptions-item label="提交时间:">{{ formatTimestamp(commit.createdAt) }}</el-descriptions-item>
+      <el-descriptions-item label="分数:">{{ commit.score }}</el-descriptions-item>
+      <el-descriptions-item label="Git Commit ID:" :span="2">{{ commit.hash }}</el-descriptions-item>
+    </el-descriptions>
+  </paper>
+<!--  <el-divider></el-divider>-->
+  <el-table :data="commit.records">
+    <el-table-column label="题目">
+      <template #default="scope">
+        <span>{{ scope.row.problem.name }}</span>
+      </template>
+    </el-table-column>
+    <el-table-column label="状态" width="280">
+      <template #default="scope">
+        <el-tag :type="buildStatusToTagType(scope.row.status)">
+          {{ buildStatusToTagContent(scope.row.status) }}
+        </el-tag>
+      </template>
+    </el-table-column>
+    <el-table-column prop="score" label="分数" width="180" />
+    <el-table-column label="操作" width="260">
+      <template #default="scope">
+        <el-button
+            type="primary"
+            size="mini"
+            @click="$router.push(`${$route.path}/record/${scope.row.id}`)"
+        >详情</el-button>
+        <el-button
+            v-if="stopRecordButton(scope.row.status)"
+            type="danger"
+            size="mini"
+            @click="dialog.stop = scope.$index"
+        >
+          停止
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+import Paper from "@/views/ProblemPage/components/Paper";
+import {formatTimestamp, buildStatusToTagType, buildStatusToTagContent} from "@/utils/coder-utils";
+
+export default {
+  name: "CommitBoard",
+  components: {Paper},
+  props: {
+    commitId: {
+      type: Number,
+      required: true
+    }
+  },
+  data() {
+    return {
+      commit: {},
+      dialog: {
+        stop: -1
+      },
+      loading: false
+    };
+  },
+  mounted() {
+    this.fetchData();
+  },
+  methods: {
+    fetchData() {
+      this.$apis.getCommitByCommitId(this.commitId)
+          .then(_res => {
+            this.commit = _res.data.res;
+            console.log(this.commit);
+          }).catch(_err => {
+        console.log(_err);
+
+      });
+    },
+    formatTimestamp,
+    buildStatusToTagType,
+    buildStatusToTagContent,
+    stopRecordButton(status) {
+      if (this.commit.createdAt !== undefined) {
+        return (
+            status === "RUNNING" &&
+            (Date.parse(new Date().toString()) / 1000 - this.commit.createdAt) /
+            60 >
+            7
+        );
+      }
+      return false;
+    }
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 82 - 0
src/views/ExamPage/ResultPage/ResultDetail.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="result-container">
+    <el-card>
+      <template #header>
+        <page-header>
+<!--          <el-button @click="backward" type="text">-->
+<!--            <el-icon size="20"><Back /></el-icon>-->
+<!--          </el-button>-->
+
+          用户 {{ user.username }} 的成绩列表
+        </page-header>
+      </template>
+      <el-table :data="result.commits">
+        <el-table-column prop="id" label="#" width="200" />
+        <el-table-column label="提交时间">
+          <template #default="scope">
+            <span>{{ formatTimestamp(scope.row.created_at) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="300">
+          <template #default="scope">
+            <span>{{ scope.row.finished ? '运行结束' : '等待或运行中' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="score" label="成绩" width="200" />
+        <el-table-column label="操作" width="150">
+          <template #default="scope">
+            <el-button
+                type="primary"
+                size="mini"
+                @click="$router.push(`${$route.path}/commit/${scope.row.id}`)"
+            >详情</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import {ElMessage} from "element-plus";
+import PageHeader from "@/components/PageHeader";
+import {formatTimestamp} from "@/utils/coder-utils";
+
+export default {
+  name: "ResultDetail",
+  components: {PageHeader},
+  data(){
+    return {
+      result: {},
+      user: {},
+    };
+  },
+  mounted() {
+    this.fetchData();
+  },
+  methods: {
+    formatTimestamp,
+    fetchData() {
+      const {resultId} = this.$route.params;
+      this.$apis.getResultByResultId(resultId)
+          .then(_res => {
+            this.result = _res.data.res ?? {};
+            this.user = this.result.owner;
+          }).catch(_err => {
+            ElMessage.error(_err);
+      });
+    },
+    backward() {
+
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.result-container {
+  margin: 40px 10vw 80px 10vw;
+  min-height: calc(100vh - 100px);
+}
+</style>

+ 221 - 0
src/views/ExamPage/ResultPage/ResultPage.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="result-container">
+    <el-card>
+      <template #header>
+        <page-header>
+          <template #actions>
+            <el-button @click="download">
+              下载成绩单
+            </el-button>
+            <el-button @click="downloadDetailed">
+              下载详细成绩单
+            </el-button>
+          </template>
+          结果列表
+        </page-header>
+      </template>
+      <paper shadow="sm" style="margin-bottom: 20px">
+        <echarts style="width: 100%" :options="chartOptions" />
+        <el-descriptions>
+          <el-descriptions-item label="总计人数">
+            {{ totalStudents }}
+          </el-descriptions-item>
+          <el-descriptions-item label="总计平均成绩">
+            {{ totalAverage }}
+          </el-descriptions-item>
+          <el-descriptions-item label="有效人数">
+            {{ committedStudents }}
+          </el-descriptions-item>
+          <el-descriptions-item label="有效平均成绩">
+            {{ committedAverage }}
+          </el-descriptions-item>
+          <el-descriptions-item label="满分人数">
+            {{ fullScoreStudents }}
+          </el-descriptions-item>
+          <el-descriptions-item label="满分比例">
+            {{ fullScoreRatio }}
+          </el-descriptions-item>
+          <el-descriptions-item label="及格人数">
+            {{ passedStudents }}
+          </el-descriptions-item>
+          <el-descriptions-item label="及格比例">
+            {{ passedRatio }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </paper>
+      <el-table :data="resultList">
+        <el-table-column prop="id" label="#" width="200" />
+        <el-table-column label="用户名">
+          <template #default="scope">
+            <span>{{ scope.row.owner.username }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="最后提交时间">
+          <template #default="scope">
+            <span v-if="!scope.row.lastCommitAt">没有提交记录</span>
+            <span v-else>{{ formatTimestamp(scope.row.lastCommitAt) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="score" label="成绩" width="200" />
+        <el-table-column label="操作" width="200">
+          <template #default="scope">
+            <el-button type="text" @click="jumpToCodemirror" disabled>源码</el-button>
+            |
+            <el-button type="text" @click="jumpToResultDetail(scope.row.id)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import PageHeader from "@/components/PageHeader";
+import Paper from "@/views/ProblemPage/components/Paper";
+import Echarts from "@/components/Echarts";
+import {ElMessage} from "element-plus";
+import {formatTimestamp} from "@/utils/coder-utils";
+
+export default {
+  name: "ResultDetail",
+  components: {Echarts, Paper, PageHeader},
+  props: {},
+  mounted() {
+    this.fetchData();
+  },
+  data() {
+    return {
+      resultList: [],
+      chartOptions: {
+        tooltip: {
+          show: true
+        },
+        xAxis: {
+          data: [
+            "未提交",
+            "0-9",
+            "10-19",
+            "20-29",
+            "30-39",
+            "40-49",
+            "50-59",
+            "60-69",
+            "70-79",
+            "80-89",
+            "90-99",
+            "100"
+          ]
+        },
+        yAxis: {
+          name: "人数"
+        },
+        series: [
+          {
+            type: "bar",
+            data: [],
+            tooltip: {
+              formatter: "{c}人"
+            }
+          }
+        ]
+      }
+    };
+  },
+  computed: {
+    summary() {
+      return this.resultList
+          .map((result) => result.score)
+          .reduce((a = 0, b = 0) => a + b, 0) ?? 0;
+    },
+    totalStudents() {
+      return this.resultList.length;
+    },
+    totalAverage() {
+      return (this.summary / Math.max(this.totalStudents, 1)).toFixed(2);
+    },
+    committedStudents() { //该同学有提交历史就算有效提交
+      return this.resultList
+          .map(result => result.lastCommitAt ? 1 : 0)
+          .reduce((a, b) => a + b, 0);
+    },
+    committedAverage() { //注意:无效提交成绩为0
+      return (this.summary / Math.max(this.committedStudents, 1)).toFixed(2);
+    },
+    fullScoreStudents() {
+      return this.resultList
+          .map(result => result.score === 100)
+          .reduce((a, b) => a + b, 0);
+    },
+    fullScoreRatio() {
+      return (
+          (
+              (this.fullScoreStudents * 100) /
+              Math.max(this.committedStudents, 1)
+          ).toFixed(2) + "%"
+      );
+    },
+    passedStudents() { //
+      return this.resultList
+          .map(result => result?.score ?? 0 >= 60)
+          .reduce((a, b) => a + b, 0);
+    },
+    passedRatio() {
+      return (
+          (
+              (this.passedStudents * 100) /
+                  Math.max(this.committedStudents, 1)
+          ).toFixed(2) + "%"
+      );
+    }
+  },
+  methods: {
+    formatTimestamp,
+    //根据考试Id, 获取考试成绩数据
+    fetchData() {
+      const { examId } = this.$route.params;
+      this.$apis.getCoderResultByExamId(examId)
+          .then(_res => {
+            this.resultList = _res.data.res ?? [];
+            console.log(this.resultList);
+            //下面是两步操作
+            //1 按照分数段分类
+            //2 根据分数段,获取每个分数段的人数(返回数组)
+            this.chartOptions.series[0].data = this.resultList.map(
+                result => (result.lastCommitAt ? Math.floor((result.score ?? 0) / 10) : -1) + 1
+            ).reduce((a, b) => {
+              a[b]++;
+              return a;
+            }, new Array(12).fill(0));
+          }).catch(_err => {
+        ElMessage.error(_err);
+      });
+    },
+
+    //TODO 下载考试成绩的认证过程,信息安全问题
+    //下载考试成绩单
+    download() {
+      const { examId } = this.$route.params;
+      window.open(`https://coder.seec.seecoder.cn/api/file/result/${examId}`);
+    },
+    //下载考试详细成绩单
+    downloadDetailed() {
+      const { examId } = this.$nuxt.$route.params;
+      window.open(`https://coder.seec.seecoder.cn/api/file/result/${examId}/detailed`);
+    },
+    jumpToCodemirror() {
+    },
+    jumpToResultDetail(resultId) { //跳转到具体结果信息页
+      this.$router.push(`${this.$route.path}/${resultId}`);
+    }
+  }
+
+};
+</script>
+
+<style lang="scss" scoped>
+.result-container {
+  margin: 40px 10vw 80px 10vw;
+  min-height: calc(100vh - 100px);
+}
+
+</style>

+ 216 - 18
src/views/ManagePage/ManagePage.vue

@@ -21,6 +21,12 @@
           </el-icon>
           <span style="padding-left: 20px">全部测试</span>
         </el-menu-item>
+        <el-menu-item index="3">
+          <el-icon>
+            <DocumentChecked/>
+          </el-icon>
+          <span style="padding-left: 20px">全部题目</span>
+        </el-menu-item>
       </el-menu>
     </el-col>
     <el-col :span="1"/>
@@ -60,10 +66,10 @@
           <span style="font-weight: bold; font-size: larger">
             全部测试
           </span>
-            <el-button class="ml-16" plain type="primary"
-                       @click="dialogExamTitle='新增测试';dialogExamVisible=true;switchExamDisabled=false;">
-              新建测试
-            </el-button>
+<!--            <el-button class="ml-16" plain type="primary"-->
+<!--                       @click="dialogExamTitle='新增测试';dialogExamVisible=true;switchExamDisabled=false;">-->
+<!--              新建测试-->
+<!--            </el-button>-->
           </div>
         </template>
         <el-tabs v-model="activeTestPaneName">
@@ -89,6 +95,77 @@
           </el-tab-pane>
         </el-tabs>
       </el-card>
+
+      <el-divider/>
+
+      <el-card>
+        <template #header>
+          <div class="card-header">
+          <span style="font-weight: bold; font-size: larger">
+            全部题目
+          </span>
+<!--            <el-button class="ml-16" plain type="primary"-->
+<!--                       @click="handleProblemCreate">-->
+<!--              新建题目-->
+<!--            </el-button>-->
+          </div>
+        </template>
+
+        <page-header>
+          <template #actions>
+            <el-input v-model="dialogProblemName"
+                      style="width:200px;"
+                      placeholder="搜索"
+            />
+            <el-select v-model="dialogProblemTypeStr" @change="fetchProblemData(1)">
+              <el-option v-for="item in problemTypeOptions" :key="item.value" :value="item.value">
+                {{item.label}}
+              </el-option>
+            </el-select>
+            <el-button @click="uploadFile" type="primary" style="margin-right: 40px">
+              <el-icon :size="14"><UploadFilled/></el-icon>
+              上传题目
+            </el-button>
+          </template>
+        </page-header>
+
+        <el-table :data="problemList" >
+          <el-table-column type="expand">
+            <template #default="props">
+              <div>{{ props.row.description }}</div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="名称"></el-table-column>
+          <el-table-column label="类型">
+            <template #default="scope">
+              <span>{{ problemTypeToTagContent(scope.row.type) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="上传时间">
+            <template #default="scope">
+              <span>{{ formatTimestamp(scope.row.createdAt)}}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="200">
+            <template #default="scope">
+              <el-button
+                  type="primary"
+                  size="mini"
+                  @click="checkDetails(scope.row.id)"
+              >
+                详情</el-button>
+          </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+            background
+            layout="prev, pager, next"
+            :current-page="problemPageInfo.page"
+            :page-size="problemPageInfo.limit"
+            :total="problemPageInfo.total"
+            @current-change="changePage"
+        />
+      </el-card>
     </el-col>
 
 <!--    创建门户课程-->
@@ -301,21 +378,79 @@
           @current-change="changePage"
       />
     </el-dialog>
+
+    <el-dialog v-model="uploadFileDialog" :width="1000">
+      <el-form
+          ref="form"
+          :model="uploadFileForm"
+          label-width="80px"
+          :rules="uploadFileRules"
+          class="fill-card"
+      >
+        <el-form-item label="题目名称" prop="name">
+          <el-input v-model="uploadFileForm.name"></el-input>
+        </el-form-item>
+        <el-form-item label="题目类型" prop="type">
+          <el-select v-model="uploadFileForm.type" placeholder="请选择" value="">
+            <el-option
+                v-for="item in problemTypeOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="题目描述" prop="description">
+          <el-input
+              v-model="uploadFileForm.description"
+              type="textarea"
+              :rows="3"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="题目文件" prop="file">
+          <el-upload
+              ref="file"
+              action=""
+              :auto-upload="false"
+              :limit="1"
+              accept=".zip"
+              :on-change="onFileChange"
+              :on-remove="onFileChange"
+          >
+              <el-button size="small" type="primary">选取</el-button>
+          </el-upload>
+        </el-form-item>
+        <el-form-item>
+          <div class="form-button">
+            <el-button type="danger" plain @click="resetForm">重置</el-button>
+            <el-button
+                type="primary"
+                :loading="loading"
+                :disabled="!uploadFiles.length"
+                @click="submitProblem"
+            >上传</el-button
+            >
+          </div>
+        </el-form-item>
+      </el-form>
+
+    </el-dialog>
   </el-row>
 </template>
 
 <script>
 import CourseCard from "@/components/CourseCard";
 import { ElMessage, ElMessageBox } from "element-plus";
-import client from "@/utils/oss";
 import { Back, Document } from "@element-plus/icons-vue";
 import TestCard from "@/components/TestCard";
 import {debounce} from "@/utils/debounce";
 import {formatTimestamp, problemTypeOptions, problemTypeToTagContent} from "@/utils/coder-utils";
+import PageHeader from "@/components/PageHeader";
 
 export default {
   name: "ManagePage",
   components: {
+    PageHeader,
     TestCard,
     Document,
     Back,
@@ -462,7 +597,24 @@ export default {
       dialogProblemTypeStr: "",
       dialogProblemName: "",
       problemList: [],
-      problemPageInfo: {}
+      problemPageInfo: {},
+
+      //上传Coder代码题
+      uploadFileDialog: false,
+      uploadFileForm: {
+        name: "",
+        type: "",
+        description: ""
+      },
+      uploadFileRules: {
+        name: [{ required: true, message: "请输入题目名称", trigger: "blur" }],
+        type: [{ required: true, message: "请选择题目类型", trigger: "blur" }],
+        description: [
+          { required: true, message: "请输入题目描述", trigger: "blur" }
+        ]
+      },
+      uploadFiles: [],
+      loading: false,
     };
   },
   computed: {
@@ -490,15 +642,7 @@ export default {
     this.$apis.getTeacherCoderExamsGroupByCourse().then(res => {
       this.coderExamList = res.data.data;
     });
-    this.$apis.getAllCoderCourses().then(_res => {
-      this.coderCourses = _res.data.res;
-      this.courseOptions = this.coderCourses.map(i => {
-        return {
-          value: i.id,
-          label: i.name
-        };
-      });
-    });
+    this.fetchProblemData();
   },
   watch: {
     //TODO 防抖!
@@ -694,15 +838,17 @@ export default {
     // handleProblemChange(){
     //   debounce(fetchProblemData,1000);
     // },
-    checkDetails(){
-      ElMessage.warning("暂不支持查看详情!");
+    checkDetails(problemId){
+      this.$router.push(`/problem/${problemId}`);
     },
     fetchProblemData(page=1) { // 按需获得Coder题目
       this.$apis.retrievePageProblem(page, this.dialogProblemLimit, this.dialogProblemTypeStr, this.dialogProblemName)
           .then(_res => {
             this.problemPageInfo = _res.data.page;
             this.problemList = _res.data.res;
-          });
+          }).catch(err => {
+        console.log(err);
+      });
     },
     removeEnable(problem) { //是否可以移除题目
       return this.$store.state.problem.cart.length && this.$store.state.problem.cart
@@ -809,7 +955,59 @@ export default {
           });
       }
     },
+    handleProblemCreate(){
+      alert("创建Coder题目");
+    },
+    uploadFile(){
+      this.uploadFileDialog = true;
+    },
+    onFileChange(fileList){
+      console.log(fileList);
+      this.uploadFiles.push(fileList);
+    },
+    resetForm() {
+      const form = this.$refs.form;
+      form.resetFields();
+      const file = this.$refs.file;
+      file.clearFiles();
+      this.uploadFiles = [];
+    },
+    submitProblem() {
+      const form = this.$refs.form;
+      form.validate((valid) => {
+        if (valid) {
+          this.uploadProblem();
+        } else {
+          return false;
+        }
+      });
+    },
+    uploadProblem() {
+      this.loading = true;
+      const body = new FormData();
+      const meta = new Blob([JSON.stringify(this.uploadFileForm)], {
+        type: "application/json",
+      });
+      body.append("meta", meta);
+      body.append("data", this.uploadFiles[0].raw);
+      this.$apis.createCoderProblem(body)
+          .then((data) => {
+            ElMessage.success("创建Coder代码题成功");
+            this.loading = false;
+            this.uploadFileDialog = false;
+            // this.problemList.push()
+            // this.$nuxt.$router.push("/problem/" + data.res?.id);
+          })
+          .catch((err) => {
+            this.$message.error({
+              message: errorMessage(err),
+              center: true
+            });
+            this.loading = false;
+          });
+    }
   },
+
 };
 </script>
 

+ 168 - 112
src/views/ProblemPage/ProblemDetail.vue

@@ -8,19 +8,21 @@
               下载
             </el-button>
             <el-button
-              variant="danger"
-              @click="this.$store.commit('removeProblem', problem)"
+                v-if="removeEnable"
+              type="danger"
+              @click="$store.commit('removeProblem', problem)"
             >移出试题清单
             </el-button
             >
             <el-button
-              variant="primary"
-              @click="this.$store.commit('addProblem', problem)"
+                v-else-if="addEnable"
+              type="primary"
+              @click="$store.commit('addProblem', problem)"
             >加入试题清单
             </el-button
             >
             <el-button
-              variant="danger-secondary"
+              type="danger"
               @click="dialog.delete = true"
             >
               删除题目
@@ -29,90 +31,57 @@
           {{ problem.name }}
         </page-header>
       </template>
-      <el-dialog v-model="dialog.delete" title="确认删除题目">
-        <b>确认删除题目吗?</b>
-        <p>题目ID:{{ problem.id }}</p>
-        <p>题目名称:{{ problem.name }}</p>
-        <template #footer>
-        <span class="dialog-footer">
-        <el-button @click="dialog.delete = false">取消</el-button>
-        <el-button
-          :loading="loading.delete"
-          type="danger"
-          @click="deleteProblem"
-        >确定</el-button
-        >
-      </span>
-        </template>
-      </el-dialog>
-      <el-dialog v-model="dialog.update" title="更新题目文件">
-        <el-form label-width="40px">
-          <el-form-item label="路径" prop="name">
-            <div>{{ selectedFile }}</div>
-          </el-form-item>
-          <el-form-item label="文件" prop="file">
-            <el-upload
-              ref="file"
-              :auto-upload="false"
-              :limit="1"
-              :on-change="onFileChange"
-              :on-remove="onFileChange"
-              action=""
-            >
-              <el-button size="small" type="primary"
-              >选取
-              </el-button
-              >
-            </el-upload>
-          </el-form-item>
-        </el-form>
-        <template #footer>
-          <span class="dialog-footer">
-          <el-button @click="cancelUpdate">取消</el-button>
-          <el-button
-            :disabled="!updateFiles.length"
-            :loading="loading.update"
-            type="primary"
-            @click="onSubmit"
-          >确定</el-button
-          >
-        </span>
-        </template>
 
-      </el-dialog>
       <paper shadow="sm" style="margin-bottom: 20px">
         <el-descriptions>
-          <el-descriptions-item label="题目名称">
+          <el-descriptions-item
+              label="题目名称:"
+              label-class-name="label-bold"
+          >
             {{ problem.name }}
           </el-descriptions-item>
-          <el-descriptions-item label="题目类型">
+          <el-descriptions-item
+              label="题目类型:"
+              label-class-name="label-bold"
+          >
             {{ typeTagContent }}
           </el-descriptions-item>
-          <el-descriptions-item label="上传者">
+          <el-descriptions-item
+              label="上传者:"
+              label-class-name="label-bold"
+          >
             {{ problem.creator ? problem.creator.username : "系统" }}
           </el-descriptions-item>
-          <el-descriptions-item label="上传时间">
-            {{ format(problem.createdAt) }}
+          <el-descriptions-item
+              label="上传时间:"
+              label-class-name="label-bold"
+          >
+            {{ formatTimestamp(problem.createdAt) }}
           </el-descriptions-item>
-          <el-descriptions-item :span="3" label="题目描述">
+          <el-descriptions-item
+              :span="3"
+              label="题目描述:"
+              label-class-name="label-bold"
+          >
             {{ problem.description }}
           </el-descriptions-item>
         </el-descriptions>
       </paper>
+
       <div class="fill-card top-divide">
         <el-table :data="files">
           <el-table-column label="文件列表">
-            <template v-slot="scope">
+            <template #default="scope">
               <span>{{ scope.row.path }}</span>
             </template>
           </el-table-column>
           <el-table-column label="类型" width="120">
-            <template v-slot="scope">
+            <template #default="scope">
               <span>{{ scope.row.type }}</span>
             </template>
           </el-table-column>
           <el-table-column label="操作" width="220">
-            <template v-slot="scope">
+            <template #default="scope">
               <el-button
                 size="small"
                 type="text"
@@ -131,24 +100,75 @@
           </el-table-column>
         </el-table>
       </div>
-      <el-dialog
+    </el-card>
+
+    <el-dialog v-model="dialog.delete" title="确认删除题目">
+      <b>确认删除题目吗?</b>
+      <p>题目ID:{{ problem.id }}</p>
+      <p>题目名称:{{ problem.name }}</p>
+      <template #footer>
+        <span class="dialog-footer">
+        <el-button @click="dialog.delete = false">取消</el-button>
+        <el-button
+            :loading="loading.delete"
+            type="danger"
+            @click="deleteProblem"
+        >确定</el-button
+        >
+      </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="dialog.update" title="更新题目文件">
+      <el-form label-width="40px">
+        <el-form-item label="路径" prop="name">
+          <div>{{ selectedFile }}</div>
+        </el-form-item>
+        <el-form-item label="文件" prop="file">
+          <el-upload
+              ref="file"
+              :auto-upload="false"
+              :limit="1"
+              :on-change="onFileChange"
+              :on-remove="onFileChange"
+              action=""
+          >
+            <el-button size="small" type="primary"
+            >选取
+            </el-button
+            >
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+          <span class="dialog-footer">
+          <el-button @click="cancelUpdate">取消</el-button>
+          <el-button
+              :disabled="!updateFiles.length"
+              :loading="loading.update"
+              type="primary"
+              @click="onSubmit"
+          >确定</el-button
+          >
+        </span>
+      </template>
+
+    </el-dialog>
+    <el-dialog
         v-model="fileDetailDialogShow"
         :title="fileDetailName"
         width="80%"
-      >
-        <pre>{{ fileDetail }}</pre>
-      </el-dialog>
-    </el-card>
+    >
+      <pre>{{ fileDetail }}</pre>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import PageHeader from "./components/PageHeader.vue";
 import Paper from "./components/Paper.vue";
-
-const { formatTimestamp } = require("@/utils/coder-utils");
-
-const { problemTypeToTagContent } = require("@/utils/coder-utils");
+import {mapGetters} from "vuex";
+import {errorMessage, formatTimestamp, problemTypeToTagContent} from "@/utils/coder-utils";
+import {ElMessage} from "element-plus";
 
 export default {
   name: "ProblemDetail",
@@ -177,35 +197,62 @@ export default {
     };
   },
   computed: {
+    ...mapGetters(["isTeacherGroup"]),
     typeTagContent() {
       return problemTypeToTagContent(this.problem.type);
-    }
-  },
-  watch: {
-    "$nuxt.$route"() {
-      this.fetchData();
-    }
+    },
+    removeEnable() {
+      return this.$store.state.problem.cart
+          .map((item) => item.id)
+          .includes(this.problem.id);
+    },
+    addEnable() {
+      return this.isTeacherGroup && this.problem.modifiedAt;
+    },
+    updateEnable(){
+      return true;
+      // const user = this.$store.state.user;
+      // const creator = this.problem.creator
+      // return (
+      //     this.problem.modifiedAt &&
+      //     (this.isAdmin || (creator && creator.id === user.id))
+      // )
+    },
+    deleteEnable() {
+      return (
+          this.isAdmin ||
+          (this.problem.creator &&
+              this.problem.creator.id === this.$store.state.user.id)
+      );
+    },
   },
   mounted() {
     this.fetchData();
   },
   methods: {
-    format(timestamp) {
-      return formatTimestamp(timestamp);
-    },
+    formatTimestamp,
     fetchData() {
-      const problemId = this.$route.params.id;
+      const { problemId } = this.$route.params;
       this.$apis.getCoderProblemById(problemId)
-        .then((res) => {
-          const data = res.data;
-          this.problem = data.res ?? {};
+        .then((_res) => {
+          this.problem = _res.data.res ?? {};
+
+          if(!this.problem.id) { // 判断题目是否存在
+            this.$message.error({
+              message: "当前题目不存在!",
+              center: true
+            });
+            this.$router.push("/manage");
+          }
+
           this.listFiles();
         })
-        .catch((err) => {
+        .catch((_err) => {
           this.$message.error({
-            message: errorMessage(err),
+            message: "当前题目不存在!",
             center: true
           });
+          this.$router.push("/manage");
         });
     },
     listFiles() {
@@ -225,23 +272,30 @@ export default {
       ];
     },
     download() {
-      const problemId = this.problem.id;
-      window.open(`/api/file/problem/${problemId}`);
+      //直接访问Coder后端获取文件
+      const { problemId } = this.$route.params;
+      window.open(`https://coder.seec.seecoder.cn/api/file/problem/${problemId}`);
     },
+
+    // 删除Coder代码题
     deleteProblem() {
-      this.$store.commit("removeProblem", this.problem);
+      this.$store.commit("removeProblem", this.problem); //先从题目购物车cart中移除
       this.loading.delete = true;
-      const problemId = this.$route.params.id;
+      const { problemId } = this.$route.params;
+
       this.$apis.deleteCoderProblem(problemId)
-        .then(() => {
-          this.$router.push("/problem");
-        })
-        .catch((err) => {
-          this.$message.error({
-            message: errorMessage(err),
-            center: true
-          });
-        });
+          .then(_res => {
+            ElMessage.success({
+              message: "删除代码题成功!",
+              center: true
+            });
+            this.$router.push("/manage");
+          }).catch(_err => {
+            this.$message.error({
+              message: "删除代码题失败!",
+              center: true
+            });
+      });
     },
     onFileChange(
       _,
@@ -254,17 +308,15 @@ export default {
       this.fileDetailName = filename;
       this.fileDetailDialogShow = false;
 
-      const problemId = this.$route.params.id;
-      this.$apis.getFileDetail(problemId, filename)
-        .then((res) => {
-          const data = res.data;
-          console.log(data);
-          this.fileDetail = data.res ?? "";
+      const { problemId } = this.$route.params;
+      this.$apis.getProblemFile(problemId, filename)
+        .then((_res) => {
+          this.fileDetail = _res.data.res ?? "";
           this.fileDetailDialogShow = true;
         })
-        .catch((err) => {
+        .catch((_err) => {
           this.$message.error({
-            message: errorMessage(err),
+            message: _err,
             center: true
           });
         });
@@ -280,7 +332,7 @@ export default {
       const body = new FormData();
       body.append("path", this.selectedFile);
       body.append("file", this.updateFiles[0].raw);
-      const problemId = this.$route.params.id;
+      const { problemId } = this.$route.params;
       this.$apis.updateCoderProblem(problemId, body)
         .then(() => {
           this.$message.success({
@@ -306,9 +358,13 @@ export default {
 };
 </script>
 
-<style>
+<style lang="scss">
 #problem-detail-body {
   margin: 40px 10vw 80px 10vw;
   min-height: calc(100vh - 100px);
 }
-</style>
+.label-bold {
+  font-weight: bold;
+  font-size: 15px;
+}
+</style>

+ 481 - 48
yarn.lock

@@ -990,11 +990,33 @@
     cssnano-preset-default "^4.0.0"
     postcss "^7.0.0"
 
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@jridgewell/resolve-uri@^3.0.3":
   version "3.0.5"
   resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c"
   integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==
 
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@jridgewell/sourcemap-codec@^1.4.10":
   version "1.4.11"
   resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
@@ -1008,6 +1030,14 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
+"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.15"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774"
+  integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.npmmirror.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -1092,6 +1122,32 @@
   dependencies:
     "@types/node" "*"
 
+"@types/eslint-scope@^3.7.3":
+  version "3.7.4"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
+  integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "8.4.6"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.6.tgz#7976f054c1bccfcf514bff0564c0c41df5c08207"
+  integrity sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
+  integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
+
+"@types/estree@^0.0.51":
+  version "0.0.51"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
+  integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
+
 "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18":
   version "4.17.28"
   resolved "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
@@ -1126,7 +1182,7 @@
   dependencies:
     "@types/node" "*"
 
-"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
+"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
   version "7.0.11"
   resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
   integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
@@ -1136,6 +1192,13 @@
   resolved "https://registry.npmmirror.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
   integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
 
+"@types/mini-css-extract-plugin@^0.9.1":
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.1.tgz#d4bdde5197326fca039d418f4bdda03dc74dc451"
+  integrity sha512-+mN04Oszdz9tGjUP/c1ReVwJXxSniLd7lF++sv+8dkABxVNthg6uccei+4ssKxRHGoMmPxdn7uBdJWONSJGTGQ==
+  dependencies:
+    "@types/webpack" "*"
+
 "@types/minimatch@*":
   version "3.0.5"
   resolved "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
@@ -1216,6 +1279,15 @@
     "@types/source-list-map" "*"
     source-map "^0.7.3"
 
+"@types/webpack@*":
+  version "5.28.0"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03"
+  integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==
+  dependencies:
+    "@types/node" "*"
+    tapable "^2.2.0"
+    webpack "^5"
+
 "@types/webpack@^4", "@types/webpack@^4.0.0":
   version "4.41.32"
   resolved "https://registry.npmmirror.com/@types/webpack/-/webpack-4.41.32.tgz#a7bab03b72904070162b2f169415492209e94212"
@@ -1483,6 +1555,16 @@
     estree-walker "^2.0.2"
     source-map "^0.6.1"
 
+"@vue/compiler-core@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.39.tgz#0d77e635f4bdb918326669155a2dc977c053943e"
+  integrity sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/shared" "3.2.39"
+    estree-walker "^2.0.2"
+    source-map "^0.6.1"
+
 "@vue/compiler-dom@3.2.33":
   version "3.2.33"
   resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.33.tgz#6db84296f949f18e5d3e7fd5e80f943dbed7d5ec"
@@ -1491,7 +1573,31 @@
     "@vue/compiler-core" "3.2.33"
     "@vue/shared" "3.2.33"
 
-"@vue/compiler-sfc@3.2.33", "@vue/compiler-sfc@^3.0.0":
+"@vue/compiler-dom@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz#bd69d35c1a48fe2cea4ab9e96d2a3a735d146fdf"
+  integrity sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==
+  dependencies:
+    "@vue/compiler-core" "3.2.39"
+    "@vue/shared" "3.2.39"
+
+"@vue/compiler-sfc@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz#8fe29990f672805b7c5a2ecfa5b05e681c862ea2"
+  integrity sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.39"
+    "@vue/compiler-dom" "3.2.39"
+    "@vue/compiler-ssr" "3.2.39"
+    "@vue/reactivity-transform" "3.2.39"
+    "@vue/shared" "3.2.39"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+    postcss "^8.1.10"
+    source-map "^0.6.1"
+
+"@vue/compiler-sfc@^3.0.0":
   version "3.2.33"
   resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.33.tgz#7ce01dc947a8b76c099811dc6ca58494d4dc773d"
   integrity sha512-H8D0WqagCr295pQjUYyO8P3IejM3vEzeCO1apzByAEaAR/WimhMYczHfZVvlCE/9yBaEu/eu9RdiWr0kF8b71Q==
@@ -1515,6 +1621,14 @@
     "@vue/compiler-dom" "3.2.33"
     "@vue/shared" "3.2.33"
 
+"@vue/compiler-ssr@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz#4f3bfb535cb98b764bee45e078700e03ccc60633"
+  integrity sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==
+  dependencies:
+    "@vue/compiler-dom" "3.2.39"
+    "@vue/shared" "3.2.39"
+
 "@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
   version "3.3.0"
   resolved "https://registry.npmmirror.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz#f9f5fb53464b0c37b2c8d2f3fbfe44df60f61dc9"
@@ -1552,48 +1666,72 @@
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
 
-"@vue/reactivity@3.2.33":
-  version "3.2.33"
-  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.33.tgz#c84eedb5225138dbfc2472864c151d3efbb4b673"
-  integrity sha512-62Sq0mp9/0bLmDuxuLD5CIaMG2susFAGARLuZ/5jkU1FCf9EDbwUuF+BO8Ub3Rbodx0ziIecM/NsmyjardBxfQ==
+"@vue/reactivity-transform@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz#da6ae6c8fd77791b9ae21976720d116591e1c4aa"
+  integrity sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==
   dependencies:
-    "@vue/shared" "3.2.33"
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.39"
+    "@vue/shared" "3.2.39"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
 
-"@vue/runtime-core@3.2.33":
-  version "3.2.33"
-  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.33.tgz#2df8907c85c37c3419fbd1bdf1a2df097fa40df2"
-  integrity sha512-N2D2vfaXsBPhzCV3JsXQa2NECjxP3eXgZlFqKh4tgakp3iX6LCGv76DLlc+IfFZq+TW10Y8QUfeihXOupJ1dGw==
+"@vue/reactivity@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.39.tgz#e6e3615fe2288d4232b104640ddabd0729a78c80"
+  integrity sha512-vlaYX2a3qMhIZfrw3Mtfd+BuU+TZmvDrPMa+6lpfzS9k/LnGxkSuf0fhkP0rMGfiOHPtyKoU9OJJJFGm92beVQ==
   dependencies:
-    "@vue/reactivity" "3.2.33"
-    "@vue/shared" "3.2.33"
+    "@vue/shared" "3.2.39"
 
-"@vue/runtime-dom@3.2.33":
-  version "3.2.33"
-  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.33.tgz#123b8969247029ea0d9c1983676d4706a962d848"
-  integrity sha512-LSrJ6W7CZTSUygX5s8aFkraDWlO6K4geOwA3quFF2O+hC3QuAMZt/0Xb7JKE3C4JD4pFwCSO7oCrZmZ0BIJUnw==
+"@vue/runtime-core@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.39.tgz#dc1faccab11b3e81197aba33fb30c9447c1d2c84"
+  integrity sha512-xKH5XP57JW5JW+8ZG1khBbuLakINTgPuINKL01hStWLTTGFOrM49UfCFXBcFvWmSbci3gmJyLl2EAzCaZWsx8g==
   dependencies:
-    "@vue/runtime-core" "3.2.33"
-    "@vue/shared" "3.2.33"
+    "@vue/reactivity" "3.2.39"
+    "@vue/shared" "3.2.39"
+
+"@vue/runtime-dom@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.39.tgz#4a8cb132bcef316e8151c5ed07fc7272eb064614"
+  integrity sha512-4G9AEJP+sLhsqf5wXcyKVWQKUhI+iWfy0hWQgea+CpaTD7BR0KdQzvoQdZhwCY6B3oleSyNLkLAQwm0ya/wNoA==
+  dependencies:
+    "@vue/runtime-core" "3.2.39"
+    "@vue/shared" "3.2.39"
     csstype "^2.6.8"
 
-"@vue/server-renderer@3.2.33":
-  version "3.2.33"
-  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.33.tgz#4b45d6d2ae10ea4e3d2cf8e676804cf60f331979"
-  integrity sha512-4jpJHRD4ORv8PlbYi+/MfP8ec1okz6rybe36MdpkDrGIdEItHEUyaHSKvz+ptNEyQpALmmVfRteHkU9F8vxOew==
+"@vue/server-renderer@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.39.tgz#4358292d925233b0d8b54cf0513eaece8b2351c5"
+  integrity sha512-1yn9u2YBQWIgytFMjz4f/t0j43awKytTGVptfd3FtBk76t1pd8mxbek0G/DrnjJhd2V7mSTb5qgnxMYt8Z5iSQ==
   dependencies:
-    "@vue/compiler-ssr" "3.2.33"
-    "@vue/shared" "3.2.33"
+    "@vue/compiler-ssr" "3.2.39"
+    "@vue/shared" "3.2.39"
 
 "@vue/shared@3.2.33":
   version "3.2.33"
   resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.33.tgz#69a8c99ceb37c1b031d5cc4aec2ff1dc77e1161e"
   integrity sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==
 
+"@vue/shared@3.2.39":
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.39.tgz#302df167559a1a5156da162d8cc6760cef67f8e3"
+  integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
+
 "@vue/web-component-wrapper@^1.2.0":
   version "1.3.0"
   resolved "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz#b6b40a7625429d2bd7c2281ddba601ed05dc7f1a"
   integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==
 
+"@webassemblyjs/ast@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
 "@webassemblyjs/ast@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@@ -1603,16 +1741,31 @@
     "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
     "@webassemblyjs/wast-parser" "1.9.0"
 
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
 "@webassemblyjs/floating-point-hex-parser@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4"
   integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==
 
+"@webassemblyjs/helper-api-error@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
 "@webassemblyjs/helper-api-error@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2"
   integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==
 
+"@webassemblyjs/helper-buffer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
 "@webassemblyjs/helper-buffer@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00"
@@ -1637,11 +1790,35 @@
   dependencies:
     "@webassemblyjs/ast" "1.9.0"
 
+"@webassemblyjs/helper-numbers@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
 "@webassemblyjs/helper-wasm-bytecode@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790"
   integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==
 
+"@webassemblyjs/helper-wasm-section@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+
 "@webassemblyjs/helper-wasm-section@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346"
@@ -1652,6 +1829,13 @@
     "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
     "@webassemblyjs/wasm-gen" "1.9.0"
 
+"@webassemblyjs/ieee754@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
 "@webassemblyjs/ieee754@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4"
@@ -1659,6 +1843,13 @@
   dependencies:
     "@xtuc/ieee754" "^1.2.0"
 
+"@webassemblyjs/leb128@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
 "@webassemblyjs/leb128@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95"
@@ -1666,11 +1857,30 @@
   dependencies:
     "@xtuc/long" "4.2.2"
 
+"@webassemblyjs/utf8@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
 "@webassemblyjs/utf8@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab"
   integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==
 
+"@webassemblyjs/wasm-edit@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/helper-wasm-section" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-opt" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    "@webassemblyjs/wast-printer" "1.11.1"
+
 "@webassemblyjs/wasm-edit@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf"
@@ -1685,6 +1895,17 @@
     "@webassemblyjs/wasm-parser" "1.9.0"
     "@webassemblyjs/wast-printer" "1.9.0"
 
+"@webassemblyjs/wasm-gen@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
 "@webassemblyjs/wasm-gen@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c"
@@ -1696,6 +1917,16 @@
     "@webassemblyjs/leb128" "1.9.0"
     "@webassemblyjs/utf8" "1.9.0"
 
+"@webassemblyjs/wasm-opt@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+
 "@webassemblyjs/wasm-opt@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61"
@@ -1706,6 +1937,18 @@
     "@webassemblyjs/wasm-gen" "1.9.0"
     "@webassemblyjs/wasm-parser" "1.9.0"
 
+"@webassemblyjs/wasm-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
 "@webassemblyjs/wasm-parser@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e"
@@ -1730,6 +1973,14 @@
     "@webassemblyjs/helper-fsm" "1.9.0"
     "@xtuc/long" "4.2.2"
 
+"@webassemblyjs/wast-printer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
 "@webassemblyjs/wast-printer@1.9.0":
   version "1.9.0"
   resolved "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899"
@@ -1757,6 +2008,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
     mime-types "~2.1.34"
     negotiator "0.6.3"
 
+acorn-import-assertions@^1.7.6:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
+  integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
+
 acorn-jsx@^5.2.0:
   version "5.3.2"
   resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -1787,6 +2043,11 @@ acorn@^8.5.0, acorn@^8.7.0:
   resolved "https://registry.npmmirror.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
   integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
 
+acorn@^8.7.1:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+  integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
 address@>=0.0.1, address@^1.0.0, address@^1.1.2:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
@@ -2397,6 +2658,16 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4
     node-releases "^2.0.2"
     picocolors "^1.0.0"
 
+browserslist@^4.14.5:
+  version "4.21.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+  integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+  dependencies:
+    caniuse-lite "^1.0.30001400"
+    electron-to-chromium "^1.4.251"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.9"
+
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -2583,6 +2854,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317:
   resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
   integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
 
+caniuse-lite@^1.0.30001400:
+  version "1.0.30001402"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001402.tgz#aa29e1f47f5055b0d0c07696a67b8b08023d14c8"
+  integrity sha512-Mx4MlhXO5NwuvXGgVb+hg65HZ+bhUYsz8QtDGDo2QmaJS2GBX47Xfi2koL86lc8K+l+htXeTEB/Aeqvezoo6Ew==
+
 case-sensitive-paths-webpack-plugin@^2.3.0:
   version "2.4.0"
   resolved "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
@@ -3660,6 +3936,13 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
+echarts@^4.8.0:
+  version "4.9.0"
+  resolved "https://registry.yarnpkg.com/echarts/-/echarts-4.9.0.tgz#a9b9baa03f03a2a731e6340c55befb57a9e1347d"
+  integrity sha512-+ugizgtJ+KmsJyyDPxaw2Br5FqzuBnyOWwcxPKO6y0gc5caYcfnEUIlNStx02necw8jmKmTafmpHhGo4XDtEIA==
+  dependencies:
+    zrender "4.3.2"
+
 ee-first@1.1.1, ee-first@~1.1.1:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -3670,6 +3953,11 @@ ejs@^2.6.1:
   resolved "https://registry.npmmirror.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
   integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
 
+electron-to-chromium@^1.4.251:
+  version "1.4.253"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.253.tgz#3402fd2159530fc6d94237f1b9535fa7bebaf399"
+  integrity sha512-1pezJ2E1UyBTGbA7fUlHdPSXQw1k+82VhTFLG5G0AUqLGvsZqFzleOblceqegZzxYX4kC7hGEEdzIQI9RZ1Cuw==
+
 electron-to-chromium@^1.4.84:
   version "1.4.113"
   resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.113.tgz#b3425c086e2f4fc31e9e53a724c6f239e3adb8b9"
@@ -3748,6 +4036,14 @@ enhanced-resolve@^4.5.0:
     memory-fs "^0.5.0"
     tapable "^1.0.0"
 
+enhanced-resolve@^5.10.0:
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
+  integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
 entities@^2.0.0:
   version "2.2.0"
   resolved "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -3800,6 +4096,11 @@ es-abstract@^1.17.2, es-abstract@^1.19.1:
     string.prototype.trimstart "^1.0.4"
     unbox-primitive "^1.0.1"
 
+es-module-lexer@^0.9.0:
+  version "0.9.3"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
+  integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
+
 es-to-primitive@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -3857,6 +4158,14 @@ eslint-plugin-vue@^7.0.0:
     semver "^6.3.0"
     vue-eslint-parser "^7.10.0"
 
+eslint-scope@5.1.1, eslint-scope@^5.0.0, eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
 eslint-scope@^4.0.3:
   version "4.0.3"
   resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
@@ -3865,14 +4174,6 @@ eslint-scope@^4.0.3:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
-eslint-scope@^5.0.0, eslint-scope@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
-  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
-  dependencies:
-    esrecurse "^4.3.0"
-    estraverse "^4.1.1"
-
 eslint-utils@^1.4.3:
   version "1.4.3"
   resolved "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
@@ -3998,7 +4299,7 @@ eventemitter3@^4.0.0:
   resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
 
-events@^3.0.0:
+events@^3.0.0, events@^3.2.0:
   version "3.3.0"
   resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
   integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@@ -4569,6 +4870,11 @@ glob-to-regexp@^0.3.0:
   resolved "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   integrity sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==
 
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
 glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
   version "7.2.0"
   resolved "https://registry.npmmirror.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
@@ -4630,7 +4936,7 @@ globby@^9.2.0:
     pify "^4.0.1"
     slash "^2.0.0"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
   version "4.2.10"
   resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
@@ -5546,6 +5852,15 @@ jest-worker@^26.5.0:
     merge-stream "^2.0.0"
     supports-color "^7.0.0"
 
+jest-worker@^27.4.5:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+  integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
 js-base64@^2.5.2:
   version "2.6.4"
   resolved "https://registry.npmmirror.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
@@ -5589,7 +5904,7 @@ json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   resolved "https://registry.npmmirror.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
-json-parse-even-better-errors@^2.3.0:
+json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
   version "2.3.1"
   resolved "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
@@ -5730,6 +6045,11 @@ loader-runner@^2.3.1, loader-runner@^2.4.0:
   resolved "https://registry.npmmirror.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
   integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
 
+loader-runner@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+  integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
 loader-utils@^0.2.16:
   version "0.2.17"
   resolved "https://registry.npmmirror.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
@@ -5993,7 +6313,7 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
   resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
   version "2.1.35"
   resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -6231,7 +6551,7 @@ negotiator@0.6.3:
   resolved "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
   integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
 
-neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
+neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
   version "2.6.2"
   resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
   integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
@@ -6292,6 +6612,11 @@ node-releases@^2.0.2:
   resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
   integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
 
+node-releases@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
+  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
+
 normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7873,7 +8198,7 @@ schema-utils@^2.0.0, schema-utils@^2.5.0, schema-utils@^2.6.5, schema-utils@^2.7
     ajv "^6.12.4"
     ajv-keywords "^3.5.2"
 
-schema-utils@^3.0.0:
+schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1:
   version "3.1.1"
   resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
   integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
@@ -7956,6 +8281,13 @@ serialize-javascript@^5.0.1:
   dependencies:
     randombytes "^2.1.0"
 
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
 serve-index@^1.9.1:
   version "1.9.1"
   resolved "https://registry.npmmirror.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
@@ -8526,6 +8858,13 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-color@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
 supports-preserve-symlinks-flag@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
@@ -8570,6 +8909,11 @@ tapable@^1.0.0, tapable@^1.1.3:
   resolved "https://registry.npmmirror.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+  integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
 tar@^6.0.2:
   version "6.1.11"
   resolved "https://registry.npmmirror.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
@@ -8612,6 +8956,17 @@ terser-webpack-plugin@^4.2.3:
     terser "^5.3.4"
     webpack-sources "^1.4.3"
 
+terser-webpack-plugin@^5.1.3:
+  version "5.3.6"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c"
+  integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==
+  dependencies:
+    "@jridgewell/trace-mapping" "^0.3.14"
+    jest-worker "^27.4.5"
+    schema-utils "^3.1.1"
+    serialize-javascript "^6.0.0"
+    terser "^5.14.1"
+
 terser@^4.1.2:
   version "4.8.0"
   resolved "https://registry.npmmirror.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
@@ -8621,6 +8976,16 @@ terser@^4.1.2:
     source-map "~0.6.1"
     source-map-support "~0.5.12"
 
+terser@^5.14.1:
+  version "5.15.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425"
+  integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
 terser@^5.3.4:
   version "5.12.1"
   resolved "https://registry.npmmirror.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c"
@@ -8938,6 +9303,14 @@ upath@^1.1.1:
   resolved "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
   integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
 
+update-browserslist-db@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz#2924d3927367a38d5c555413a7ce138fc95fcb18"
+  integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
 upper-case@^1.1.1:
   version "1.1.3"
   resolved "https://registry.npmmirror.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
@@ -9144,6 +9517,18 @@ vue-hot-reload-api@^2.3.0:
   resolved "https://registry.npmmirror.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
   integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
 
+vue-loader-v16@^16.0.0-beta.5.4:
+  version "16.0.0-beta.5.4"
+  resolved "https://registry.yarnpkg.com/vue-loader-v16/-/vue-loader-v16-16.0.0-beta.5.4.tgz#8fbcf87dc69c9979ecd777adba4e8a0e7fd95a7e"
+  integrity sha512-sis7XW0hab2NRbWMGbYXlFwhaLE2sV/hqhVkLxV2/8XLsLL6Ylfz590IaJMSMiyqsEmFHW+fSfevnnIqOsYxoQ==
+  dependencies:
+    "@types/mini-css-extract-plugin" "^0.9.1"
+    chalk "^3.0.0"
+    hash-sum "^2.0.0"
+    loader-utils "^1.2.3"
+    merge-source-map "^1.1.0"
+    source-map "^0.6.1"
+
 "vue-loader-v16@npm:vue-loader@^16.1.0":
   version "16.8.3"
   resolved "https://registry.npmmirror.com/vue-loader/-/vue-loader-16.8.3.tgz#d43e675def5ba9345d6c7f05914c13d861997087"
@@ -9184,16 +9569,16 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.npmmirror.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 
-vue@^3.0.0:
-  version "3.2.33"
-  resolved "https://registry.npmmirror.com/vue/-/vue-3.2.33.tgz#7867eb16a3293a28c4d190a837bc447878bd64c2"
-  integrity sha512-si1ExAlDUrLSIg/V7D/GgA4twJwfsfgG+t9w10z38HhL/HA07132pUQ2KuwAo8qbCyMJ9e6OqrmWrOCr+jW7ZQ==
+vue@^3.1.0:
+  version "3.2.39"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.39.tgz#de071c56c4c32c41cbd54e55f11404295c0dd62d"
+  integrity sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==
   dependencies:
-    "@vue/compiler-dom" "3.2.33"
-    "@vue/compiler-sfc" "3.2.33"
-    "@vue/runtime-dom" "3.2.33"
-    "@vue/server-renderer" "3.2.33"
-    "@vue/shared" "3.2.33"
+    "@vue/compiler-dom" "3.2.39"
+    "@vue/compiler-sfc" "3.2.39"
+    "@vue/runtime-dom" "3.2.39"
+    "@vue/server-renderer" "3.2.39"
+    "@vue/shared" "3.2.39"
 
 vuex@^4.0.2:
   version "4.0.2"
@@ -9220,6 +9605,14 @@ watchpack@^1.7.4:
     chokidar "^3.4.1"
     watchpack-chokidar2 "^2.0.1"
 
+watchpack@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
 wbuf@^1.1.0, wbuf@^1.7.3:
   version "1.7.3"
   resolved "https://registry.npmmirror.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
@@ -9334,6 +9727,11 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
 webpack@^4.0.0, webpack@^4.x:
   version "4.46.0"
   resolved "https://registry.npmmirror.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542"
@@ -9363,6 +9761,36 @@ webpack@^4.0.0, webpack@^4.x:
     watchpack "^1.7.4"
     webpack-sources "^1.4.1"
 
+webpack@^5:
+  version "5.74.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
+  integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
+  dependencies:
+    "@types/eslint-scope" "^3.7.3"
+    "@types/estree" "^0.0.51"
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/wasm-edit" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    acorn "^8.7.1"
+    acorn-import-assertions "^1.7.6"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.10.0"
+    es-module-lexer "^0.9.0"
+    eslint-scope "5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.9"
+    json-parse-even-better-errors "^2.3.1"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.1.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.3"
+    watchpack "^2.4.0"
+    webpack-sources "^3.2.3"
+
 webpackbar@^5.0.2:
   version "5.0.2"
   resolved "https://registry.npmmirror.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570"
@@ -9586,3 +10014,8 @@ yorkie@^2.0.0:
     is-ci "^1.0.10"
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
+
+zrender@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/zrender/-/zrender-4.3.2.tgz#ec7432f9415c82c73584b6b7b8c47e1b016209c6"
+  integrity sha512-bIusJLS8c4DkIcdiK+s13HiQ/zjQQVgpNohtd8d94Y2DnJqgM1yjh/jpDb8DoL6hd7r8Awagw8e3qK/oLaWr3g==