2021SC@SDUSC
文章目录
- 组件介绍
- 组件内部模块
- 核心功能
-
- 通过压缩包上传测试点数据
- 手动输入单个测试点
- 下载测试点
组件介绍
- 组件名称:testPoint
- 组件绝对路径:src / components / newProblem / testPoint.vue
- 主要功能:教师对特定题目上传、修改测试点
组件内部模块
该模块组成部分较为简单,仅由一个 el-card
作为主体容器,内含 el-table
表格组件用以展示测试点数据。
以下是 template 部分代码:
<template><div><el-card><div slot="header" class="text"><div style="text-align: left;"><el-button @click="$emit('update:step', 1);" plain>上一步</el-button><el-button :disabled="points.length===0" style="float: right; margin-right: 20px" @click="$emit('update:step', 3)" plain>下一步</el-button></div></div><div><div style="margin-bottom: 20px;"><el-upload action='' ref="upload" :show-file-list="false" :on-change="analyzeZip" :auto-upload="false"><el-button slot="trigger" style="margin-left: 10px;" size="mini" type="success">上传测试数据<i class="el-icon-upload"></i></el-button><el-button style="margin-left: 10px;" size="mini" type="primary" @click="downloadTestPoints">下载测试数据<i class="el-icon-download"></i></el-button></el-upload></div><el-table v-loading="loading" :data="points" style="width: 100%" border stripe><el-table-column prop="testName" label="名称" width="180"></el-table-column><el-table-column prop="score" label="分数" width="50"></el-table-column><el-table-column prop="tip" label="提示"></el-table-column><el-table-column label="操作" width="305"><template slot-scope="scope"><el-button type="danger" @click="remove(scope.$index)" size="small"><i class="el-icon-delete"></i>移除</el-button><el-button type="primary" @click="isUpdating = true; updateIndex = scope.$index" size="small"><i class="el-icon-edit"></i>更新</el-button></template></el-table-column><template slot="append"><el-button plain style="width: calc(100% - 1px); margin-bottom: 1px;" @click="isAdding = true;"><i class="el-icon-circle-plus-outline"></i>增加测试点</el-button></template></el-table></div></el-card><el-dialog title="更新测试点" :visible.sync="isUpdating" @open="initForm" :append-to-body="true"><el-form :model="updateForm" ref="updateForm" :rules="updateRules"><el-form-item prop="testName" label="名称"><el-input v-model="updateForm.testName"></el-input></el-form-item><el-form-item prop="score" label="分数"><el-input v-model="updateForm.score"></el-input></el-form-item><el-form-item prop="tip" label="提示"><el-input v-model="updateForm.tip"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button type="primary" @click="update">确 定</el-button></div></el-dialog><el-dialog title="增加测试点" :visible.sync="isAdding" @open="$refs.addForm?$refs.addForm.resetFields():''" :append-to-body="true"><el-form :model="addForm" ref="addForm" :rules="addRules"><el-form-item prop="testName" label="名称"><el-input v-model="addForm.testName"></el-input></el-form-item><el-form-item prop="score" label="分数"><el-input v-model="addForm.score"></el-input></el-form-item><el-form-item prop="in" label="测试点输入"><el-input v-model="addForm.in" type="textarea"></el-input></el-form-item><el-form-item prop="out" label="测试点输出"><el-input v-model="addForm.out" type="textarea"></el-input></el-form-item><el-form-item prop="tip" label="提示"><el-input v-model="addForm.tip"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button type="primary" @click="addPoint">确 定</el-button></div></el-dialog></div>
</template>
在执行某些操作时,会打开相应的 el-dialog
模态对话框,教师可在模态对话框内完成部分操作。
核心功能
该模块视图层对应的代码逻辑还是很简单的,核心代码主要在该模块的功能部分。
在这个模块中,教师可以通过手动编辑所需数据以生成特定的代码用测试点;也可通过上传一定格式的压缩文件,由前端进行解析,获取测试点数据进行上传。接下来我将对上述功能做一个说明。
通过压缩包上传测试点数据
为了方便讲解后续代码,我们先讲解如何通过上传压缩包并解析其中的测试点数据。
我们约定好,压缩包的格式为 .zip 文件。前端要想对 zip 文件进行解析,需要用到一个 js 库 —— JSZIP 。关于这个库的使用方式,在我之前的博客中有提到,这里不再赘述。
【sduoj】前端JSZip库的使用
接下来,我们来了解一下教师用户上传 zip 文件的流程。让我们先把目光聚焦到视图层的这一部分:
<div style="margin-bottom: 20px;"><el-upload action='' ref="upload" :show-file-list="false" :on-change="analyzeZip" :auto-upload="false"><el-button slot="trigger" style="margin-left: 10px;" size="mini" type="success">上传测试数据<i class="el-icon-upload"></i></el-button><el-button style="margin-left: 10px;" size="mini" type="primary" @click="downloadTestPoints">下载测试数据<i class="el-icon-download"></i></el-button></el-upload>
</div>
我们使用 Element UI 提供的 el-upload
组件实现文件上传的功能。
首先,el-upload
组件需要一个上传的地址作为参数 action
的值,但由于我们的上传部分并不依赖该组件自己的提交方法,因此我们可以随便定义 action
的值,这里我直接传入了一个空字符串。同时,由于 el-upload
组件默认是自动提交,为了阻止其一上传 zip 就将文件提交,我们为 auto-upload
参数传入 false
以阻止其自动提交至服务器。当用户通过该组件上传了一个压缩包文件后,其内部的 file-list
发生改变,调用传入 on-change
的 analyzeZip
方法,对文件进行解析。
analyzeZip(file) {
const JSZip = require("jszip");let jszip = new JSZip();jszip.loadAsync(file.raw).then(async (zip) => {
let files = Object.keys(zip.files);let allIn = true;let points = [];zip.file("scores.txt").async("string").then((text) => {
let arr = text.split("\n");for (let i = 0; i < arr.length; i++) {
if (arr[i] === "") {
continue;}let lnSplit = arr[i].split(" ");let name = lnSplit[0];let score = lnSplit[1];let tip = lnSplit[2].substring(1,lnSplit[2].length - 1);points.push({
testName: name,score: score,tip: tip,time: new Date().toLocaleString(),});let inName = name + ".in";let outName = name + ".out";if (files.indexOf(inName) === -1 ||files.indexOf(outName) === -1) {
allIn = false;}}if (allIn) {
new Promise((resolve) => {
let index = 0,length = points.length;let uploadRequests = [];points.forEach((point) => {
let name = point.testName;let inFile, outFile;Promise.all([zip.file(name + ".in").async("blob"),zip.file(name + ".out").async("blob"),]).then(([inContent, outContent]) => {
inFile = new window.File([inContent],name + ".in");outFile = new window.File([outContent],name + ".out");let formData = new FormData();formData.append("problemId",this.problemId);formData.append("testName", name);formData.append("inFile", inFile);formData.append("outFile", outFile);formData.append("score",point.score);formData.append("tip", point.tip);uploadRequests.push(this.$ajax.post("/testPoint/addTestPoint",formData,{
headers: {
Authorization:`Bearer ${
localStorage.getItem("token")}`,"Content-Type":"multipart/form-data",},}));if (++index === length) {
resolve(uploadRequests);}});});}).then((uploadRequests) => {
Promise.all(uploadRequests).then((ress) => {
for (let i = 0; i < ress.length; i++) {
let res = ress[i];if (res.data.code === 0) {
this.points.push(JSON.parse(JSON.stringfy(points[i])));}}});});} else {
this.$notify({
message: "上传的文件缺少部分测试点",type: "error",});}});}).catch(() => {
this.$notify({
message: "上传的文件有问题",type: "error",});});
}
el-upload
组件的 on-change
函数,传入的第一个值为发生改变的文件。我们先实例化一个 JSZip 对象,通过 jszip.loadAsync(file.raw)
异步读取该压缩文件的数据。
在正式解析压缩文件之前,我们先来规定一下合法的测试点压缩文件的格式。
├── aaa.in
├── aaa.out
├── bbb.in
├── bbb.out
└── scores.txt
以上是一个典型的测试点压缩文件的内部结构,其中 scores.txt 记载了所有的测试点以及它们对应的分数及测试点提示。比如这样:
aaa 10 #asdad#
bbb 10 ##
每一行都对应了一条测试点,测试点的数据以空格进行分割。第一段是测试点名称,第二段是测试点对应的分数,第三段的测试点的提示信息。
而这些测试点则体现在除 scores.txt 外的其他文件上。每个测试点都对应了两个以测试点名称命名的文件,一个后缀为 .in ,另一个后缀为 .out 。分别对应了该测试点的输入及输出。
在了解了压缩包的内部结构后,我们再将目光回到 analyzeZip
函数中。在调用 jszip.loadAsync(file.raw)
后,我们在该 api 的 .then 中编写后续的代码。我们将回调的参数命名为 zip ,首先获取 zip 中的所有文件, let files = Object.keys(zip.files)
。接下来我们来读取 scores.txt ,判断是否所有测试点都被包含在压缩文件中。
let allIn = true; // 默认所有测试点文件都存在
let points = []; // 初始化测试点数据数组
zip.file("scores.txt").async("string").then((text) => {
// 以 string 格式读取 scores.txtlet arr = text.split("\n"); // 按行分割文本内容for (let i = 0; i < arr.length; i++) {
// 遍历每一行内容if (arr[i] === "") {
// 如果该行内容为空,直接跳过该行continue;}let lnSplit = arr[i].split(" "); // 按空格分割每一行内容let name = lnSplit[0]; // 第一段是测试点名称let score = lnSplit[1]; // 第二段是测试点分数let tip = lnSplit[2].substring(1,lnSplit[2].length - 1); // 第三段是由两个 “#” 包裹起来的测试点描述points.push({
testName: name,score: score,tip: tip,time: new Date().toLocaleString(),}); // 向测试点数组中添加该行对应的测试点数据let inName = name + ".in";let outName = name + ".out";if (files.indexOf(inName) === -1 ||files.indexOf(outName) === -1) {
allIn = false; // 一旦有测试点对应的 in / out 文件不存在压缩包中,将 allIn 赋为 false}}/*** 后续代码*/})
当 allIn
为真时,我们就可以在 后续代码 中对测试点文件进行解析,并准备向后端发送 HTTP 请求。
/*** 后续代码如下*/
if (allIn) {
new Promise((resolve) => {
let index = 0,length = points.length;let uploadRequests = []; // 初始化 http 请求数组points.forEach((point) => {
// 遍历测试点数组let name = point.testName;let inFile, outFile;/*** 通过 Promise.all 同时读取 in / out 文件内容*/Promise.all([zip.file(name + ".in").async("blob"),zip.file(name + ".out").async("blob")]).then(([inContent, outContent]) => {
/*** 根据读取的二进制文件内容生成新的输入输出文件*/inFile = new window.File([inContent],name + ".in");outFile = new window.File([outContent],name + ".out");/*** 编辑 FormData 用于发送请求*/let formData = new FormData();formData.append("problemId",this.problemId);formData.append("testName", name);formData.append("inFile", inFile);formData.append("outFile", outFile);formData.append("score",point.score);formData.append("tip", point.tip);uploadRequests.push(this.$ajax.post("/testPoint/addTestPoint",formData,{
headers: {
Authorization: `Bearer ${
localStorage.getItem("token")}`,"Content-Type": "multipart/form-data", // 以 “multipart/form-data” 格式发送二进制数据},})); // 将请求添加至请求数组中if (++index === length) {
// 遍历完毕后,调用 Promise 对象的 resolve 函数,将请求数组传递给 then 的回调函数resolve(uploadRequests);}});});}).then((uploadRequests) => {
Promise.all(uploadRequests).then((ress) => {
// 同时发送所有的上传请求for (let i = 0; i < ress.length; i++) {
let res = ress[i];if (res.data.code === 0) {
this.points.push(JSON.parse(JSON.stringify(points[i]))); // 当该请求成功时,将其对应的测试点数据添加至数据层中}}});});
} else {
this.$notify({
message: "上传的文件缺少部分测试点",type: "error",});
}
手动输入单个测试点
当看完上面的代码后,手动上传单个测试点的代码便很好理解了。
addPoint() {
this.$refs.addForm.validate(async (valid) => {
if (valid) {
this.isAdding = false;let inFile = new window.File([this.addForm.in],this.addForm.testName + ".in");let outFile = new window.File([this.addForm.out],this.addForm.testName + ".out");/*** 编辑 FormData 用于发送请求*/let formData = new FormData();formData.append("problemId",this.problemId);formData.append("testName", this.addForm.testName);formData.append("inFile", inFile);formData.append("outFile", outFile);formData.append("score",this.addForm.score);formData.append("tip", this.addForm.tip);let res = await this.$ajax.post("/testPoint/addTestPoint",formData,{
headers: {
Authorization: `Bearer ${
localStorage.getItem("token")}`,"Content-Type": "multipart/form-data",},});if (res.data.code == 0) {
this.$message({
message: "添加成功",type: "success",showClose: false,duration: 1000,});this.points.push(JSON.parse(JSON.stringify(this.addForm)));} else {
this.$message({
message: "添加失败",type: "error",showClose: false,duration: 1000,});}}});
},
下载测试点
教师还可通过该接口以压缩包的方式下载该题对应的测试点数据。
代码如下:
async downloadTestPoints() {
let res = await this.$ajax.post("/testPoint/downLoadTestFile",{
problemId: this.problemId,},{
headers: {
Authorization:"Bearer " + localStorage.getItem("token"),},responseType: "blob", // 以 blob 类型接收后端发回的响应数据});const content = res.data; // 接收响应内容const blob = new Blob([content]); // 构造一个blob对象来处理数据let fileName = `测试点${
this.problemId}.zip`;// 对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性// IE10以上支持blob但是依然不支持downloadif ("download" in document.createElement("a")) {
//支持a标签download的浏览器const link = document.createElement("a"); // 创建a标签link.download = fileName; // a标签添加属性link.style.display = "none";link.href = URL.createObjectURL(blob);document.body.appendChild(link);link.click(); // 执行下载URL.revokeObjectURL(link.href); // 释放urldocument.body.removeChild(link); // 释放标签} else {
// 其他浏览器navigator.msSaveBlob(blob, fileName);}
},