当前位置: 代码迷 >> 综合 >> 【sduoj】出题功能模块分析 (1) --- 题目详情
  详细解决方案

【sduoj】出题功能模块分析 (1) --- 题目详情

热度:39   发布时间:2023-11-26 10:54:44.0

2021SC@SDUSC

文章目录

  • 组件介绍
  • 组件内部模块
    • 主容器
    • 基本信息表单
    • 题面描述
    • 表单提交
  • 组件复用

组件介绍

  • 组件名称:publishQuestion
  • 组件绝对路径:src / components / newProblem / publishQuestion.vue
  • 主要功能:用以编辑当前题目的基本信息

组件内部模块

该组件同样由若干其他组件共同构成,在这一段落中,我将依次介绍构成该页面的各个组件。

主容器

该组件通过 Element UIel-card构成。其由头部及主体两部分构成,而大部分功能性模块均部署于其主体部分。以下是其源码视图:

<el-card class="box-card" shadow="hover"><div slot="header"><span>编辑题目</span></div><div><!-- 这里是el-card的主体部分,该模块的所有功能性组件均位于此处 --></div>
</el-card>

基本信息表单

该组件用于教师编辑当前题目的基本信息:

  • 题目标题
  • 题目难度
  • 题目所涉及知识点(题目标签)
    该组件通过 Element UIel-form 控件实现。其视图层代码如下:
<div class="problemBasicInfo"><el-form ref="BasicInfo" :model="BasicInfo" class="basicInfo" :rules="basicRules"><div class="text"><h3>基本信息</h3></div><el-form-item label="标题" prop="title"><el-input placeholder="请输入题目标题" v-model="BasicInfo.title" /></el-form-item><el-form-item label="难度" prop="difficulty"><el-rate style="text-align: left; margin-top: 0.75em;" v-model="BasicInfo.difficulty" /></el-form-item><el-form-item label="标签:" prop="myTags"><div style="text-align: left;"><el-tag effect="dark" style="margin-left: 10px; margin-bottom: 1em;" v-for="item in BasicInfo.myTags" :key="item.index" closable :disable-transitions="false" @close="deteleTag(item)">{
   {item.value}}</el-tag><el-autocomplete placeholder="ESC退出编辑" style="margin-left: 10px; margin-bottom: 1em; width: 10em;" class="input-new-tag" v-if="tagInputVisible" v-model="tagInputValue" ref="saveTagInput" size="small" @keydown.native.esc="tagInputVisible=false; tagInputValue='';" @select="selectTag" :fetch-suggestions="querySearch"></el-autocomplete><el-button plain style="margin-left: 10px; margin-bottom: 1em; width: 10em;" v-else class="button-new-tag" size="mini" @click="showInput">New Tag</el-button></div></el-form-item></el-form>
</div>

通过视图层源码可以看出我们将该题目的基本信息绑定到了 BasicInfo 当中。通过 Vue 的双向数据绑定,当教师操作修改渲染在浏览器的 UI 组件时,数据层会自动更新更新后的数据。
在这个部分中,比较值得一提的是设置题目标签的一部分内容。下面让我们来仔细分析一下标签部分的代码实现。

<!-- 视图层 -->
<div style="text-align: left;"><el-tag effect="dark" style="margin-left: 10px; margin-bottom: 1em;" v-for="item in BasicInfo.myTags" :key="item.index" closable :disable-transitions="false" @close="deteleTag(item)">{
   {item.value}}</el-tag><el-autocomplete placeholder="ESC退出编辑" style="margin-left: 10px; margin-bottom: 1em; width: 10em;" class="input-new-tag" v-if="tagInputVisible" v-model="tagInputValue" ref="saveTagInput" size="small" @keydown.native.esc="tagInputVisible=false; tagInputValue='';" @select="selectTag" :fetch-suggestions="querySearch" /><el-button plain style="margin-left: 10px; margin-bottom: 1em; width: 10em;" v-else class="button-new-tag" size="mini" @click="showInput">New Tag</el-button>
</div>
/*** 数据层代码* 以下代码仅关注操作 tag 的部分,不代表项目源代码*/
export default {
    data() {
    return {
    BasicInfo: {
    title: "",difficulty: 1,myTags: [],},optionalTags: [],tagInputVisible: false,tagInputValue: "",}},methods: {
    // 获取可选标签async getAllTags() {
    let res = await this.$ajax.post("/problem/getAllTag",{
    },{
    headers: {
    Authorization: `Bearer ${
      localStorage.getItem("token")}`,},});if (res.data.code === 0 && res.data.message === "ok") {
    let length = res.data.data[0].length;for (let i = 0; i < length; i++) {
    this.optionalTags.push({
    value: res.data.data[0][i],index: res.data.data[1][i],});}}},showInput() {
    this.tagInputVisible = true;this.$nextTick(() => {
    this.$refs.saveTagInput.$refs.input.focus();});},querySearch(queryString, cb) {
    let optionalTags = this.optionalTags.filter((optionalTag) => {
    return this.BasicInfo.myTags.indexOf(optionalTag) === -1;});let results = queryString? optionalTags.filter((optionalTag) => {
    return optionalTag.value.indexOf(queryString) !== -1;}): optionalTags;// 调用 callback 返回建议列表的数据cb(results);},selectTag(item) {
    this.BasicInfo.myTags.push(item);this.tagInputVisible = false;this.tagInputValue = "";},deteleTag(item) {
    this.BasicInfo.myTags.splice(this.BasicInfo.myTags.indexOf(item),1);},},created() {
    this.getAllTags();}
}

结合以上两部分代码,我们可以大致得知该组件处理题目标签的逻辑。
首先,在该组件被创建之后,直接调用 getAllTags 函数。从后端获取所有可选的标签,将获取的数据格式化后加入到 data 中的 optionalTags 字段内。
在视图层,根据 BasicInfo.myTags 的数据,循环渲染,展示已经选择的 tag。每个 tag 后都跟了一个关闭按钮,可通过点击关闭按钮触发 el-tag 控件的 @close 回调。而在其 @close 回调中,触发我们在 methods 中定义的 deleteTag 函数,向内传入的参数为该标签对应的 json 对象,并通过该函数将传入的对象从 BasicInfo.myTags 中移除。
在生成的所有标签后,我们定义了一个按钮,用来增加新的标签。当点击该按钮时,将触发 showInput 函数。该函数会将标签输入框展示在页面当中,在输入框渲染完毕后,通过 javascript 将页面焦点聚焦至输入框。而该输入框具有自动补全功能,其自动补全通过 querySearch 函数实现。

querySearch(queryString, cb) {
    let optionalTags = this.optionalTags.filter((optionalTag) => {
    return this.BasicInfo.myTags.indexOf(optionalTag) === -1;});let results = queryString? optionalTags.filter((optionalTag) => {
    return optionalTag.value.indexOf(queryString) !== -1;}): optionalTags;// 调用 callback 返回建议列表的数据cb(results);
}

首先,我们通过 filter 构建一个过滤器,用以过滤所有可选的标签中已经被选中的 tag,并将剩下的 tag 赋值给该函数作用域下的 optionalTags。之后,再次使用过滤器,对输入框中输入的字符串进行模糊搜索,最后通过 callback 返回匹配的所有标签。

题面描述

该部分使用到了一个开源的 markdown 编辑器 —— mavon-editor。以下是该部分的源代码(以下代码仅关注踢面描述部分,非该模块全部源代码):

<template><el-form ref="Description" :model="Description" class="description" :rules="descriptionRules"><el-form-item label="题面" prop="text"><mavon-editor v-model="Description.text" class="markdown" fontSize="16px" @change="changeData" /></el-form-item></el-form>
</template><script> import Vue from "vue"; import mavonEditor from "mavon-editor"; import "mavon-editor/dist/css/index.css"; Vue.use(mavonEditor);export default {
      data() {
      return {
      Description: {
      text:"这是一个编程题模板。请在这里写题目描述。例如:本题目要求读入2个整数A和B,然后输出它们的和。\n" +"\n" +"### 输入格式:\n" +"\n" +"请在这里写输入格式。例如:输入在一行中给出2个绝对值不超过1000的整数A和B。\n" +"\n" +"### 输出格式:\n" +"\n" +"请在这里描述输出格式。例如:对每一组输入,在一行中输出A+B的值。\n" +"\n" +"### 输入样例:\n" +"\n" +"在这里给出一组输入。例如:\n" +"\n" +"```in\n" +"18 -299\n" +"```\n" +"\n" +"### 输出样例:\n" +"\n" +"在这里给出相应的输出。例如:\n" +"\n" +"```out\n" +"-281\n" +"```",},descriptionRules: {
      text: [{
      required: true,message: "题面描述不能为空哦",trigger: "blur",},],},} } </script>

我们首先从 node_modules/mavon-editor 中引入 mavonEditor 控件以及所需要的层叠样式表,使用 Vue.use(mavonEditor) 注册该组件,这样我们就能在 template 中使用该组件。将该组件的内容双向绑定至 Description.text 上。

表单提交

在上述所有组件中,我们用到了两个表单,每个表单都有其绑定的数据以及校验规则。下面我们来关注一下如何将这两个表单中的数据提交给后端。

<template><!-- 基本信息表单(标题,难度,标签) --><el-form ref="BasicInfo" :model="BasicInfo" class="basicInfo" :rules="basicRules" /><!-- 题面描述表单(markdwon编辑器) --><el-form ref="Description" :model="Description" class="description" :rules="descriptionRules" />
</template><script>export default {
      data() {
      return {
      BasicInfo: {
      title: "",difficulty: 1,myTags: [],},Description: {
      text: "",},basicRules: {
      title: [{
      required: true,message: "标题不能为空哦",trigger: "blur",},{
      min: 1,max: 80,message: "长度在1-80个字符",trigger: "blur",},],difficulty: [{
      required: true,message: "难度不能为空哦",trigger: "blur",},],myTags: [{
      required: true,message: "请选择至少一个标签",trigger: "none",},],},descriptionRules: {
      text: [{
      required: true,message: "题面描述不能为空哦",trigger: "blur",},],},};},methods: {
      // 一个封装好的方法用于验证表单validateForms(formRefs) {
      let objectList = [];let results = formRefs.map((formRef) =>new Promise((resolve, reject) => {
      formRef.validate((valid, object) => {
      if (valid) {
      resolve();} else {
      objectList.push(object);reject();}});}));return Promise.all(results).catch(() => {
      return Promise.reject(objectList);});},// 验证表单onSubmit() {
      let formRefs = ["BasicInfo", "Description"].map((key) => this.$refs[key]);this.validateForms(formRefs).then(() => {
      // 提交信息至后端服务器,并进行后续操作}).catch(() => {
      this.$message({
      message: `提交失败,请再来一次`,type: "error",});});}, </script>

可以看到,我们通过封装了一个函数用以同时验证若干个表单的方法,并在提交表单的函数中对其进行了调用。当所有表单校验通过后便将题目信息发送至后端,并进行后续操作。

以上便是构成该模块的所有组件。

组件复用

基于 Vue 的模块化开发思想,该模块还可用于教师修改已创建的题目的基本信息使用。在该模块内部定义了一个函数:

export default {
    props: {
    problemId: Number,},methods: {
    async getProblemInfoById() {
    let res = await this.$ajax.post("/problem/getProblemById",{
    id: this.problemId,},{
    headers: {
    Authorization: `Bearer ${
      localStorage.getItem("token")}`,},});if (res.data.code === 0) {
    let data = res.data.data;this.BasicInfo.title = data.title;this.BasicInfo.difficulty = data.difficulty;let tags = [];this.optionalTags.forEach((tag) => {
    if (data.tagList.indexOf(tag.value) !== -1) {
    tags.push(tag);}});this.BasicInfo.myTags = tags;}},},created() {
    this.getAllTags().then(() => {
    if (this.problemId !== 0) {
    this.getProblemInfoById();}});},
}

当处于修改已生成的题目信息时,本模块将在创建后调用该函数,通过题目 Id 获取题目的基本信息,并将其提供给该模块的数据层。