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 获取题目的基本信息,并将其提供给该模块的数据层。