2021SC@SDUSC
组件介绍
- 组件名称:publishQuestion
- 组件绝对路径:src / components / newProblem / publishQuestion.vue
- 主要功能:用以编辑当前题目的基本信息
组件内部模块
该组件同样由若干其他组件共同构成,在这一段落中,我将依次介绍构成该页面的各个组件。
主容器
该组件通过 Element UI 的 el-card
构成。其由头部及主体两部分构成,而大部分功能性模块均部署于其主体部分。以下是其源码视图:
<el-card class="box-card" shadow="hover">
<div slot="header">
<span>编辑题目</span>
</div>
<div>
<!-- 这里是el-card的主体部分,该模块的所有功能性组件均位于此处 -->
</div>
</el-card>
基本信息表单
该组件用于教师编辑当前题目的基本信息:
- 题目标题
- 题目难度
- 题目所涉及知识点(题目标签)
该组件通过 Element UI 的el-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 获取题目的基本信息,并将其提供给该模块的数据层。