【sduoj】Analysis of question function modules (2) --- Test point upload

2021SC@SDUSC

Component introduction

  • Component name : testPoint
  • Component absolute path : src/components/newProblem/testPoint.vue
  • Main functions : Teachers upload and modify test points for specific topics

Component internal modules

The components of this module are relatively simple, with only one el-cardas the main container, which contains el-tablea table component to display test point data.
The following is part of the template code:

<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>

When some operations are performed, the corresponding el-dialogmodal , and the teacher can complete some operations in the modal dialog box.

Core functions

The code logic corresponding to the view layer of this module is still very simple, and the core code is mainly in the functional part of this module.
In this module, teachers can manually edit the required data to generate specific code test points; they can also upload a compressed file in a certain format, which will be parsed by the front end to obtain test point data for upload. Next I will give an explanation of the above functions.

Upload test point data through compressed package

In order to facilitate the explanation of the subsequent code, we first explain how to upload the compressed package and parse the test point data in it.
We agreed that the format of the compressed package is a .zip file. If the front end wants to parse the zip file, it needs to use a js library - JSZIP . Regarding the use of this library, I mentioned it in my previous blog, so I won't go into details here.

[sduoj] Use of front-end JSZip library

Next, let's take a look at the process of teacher users uploading zip files. Let's focus on this part of the view layer first:

<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>

We use the components provided by Element UI to implement the file upload function. First of all, the component needs an uploaded address as the parameter value, but since our upload part does not depend on the component's own submission method, we can define the value of at will, here I directly pass in an empty string. At the same time, since the component is automatically submitted by default, in order to prevent it from submitting the file as soon as the zip is uploaded, we pass in the parameter to prevent it from being automatically submitted to the server. When the user uploads a compressed package file through this component, its internal is changed, and the method passed in is called to parse the file.el-upload
el-uploadactionactionel-uploadauto-uploadfalsefile-liston-changeanalyzeZip

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-uploadThe component's on-changefunction , the first value passed in is the changed file. We first instantiate a JSZip object, and read the data of the compressed file jszip.loadAsync(file.raw)asynchronously .
Before formally parsing the compressed file, let's first specify the format of the legal test point compressed file.

├── aaa.in
├── aaa.out
├── bbb.in
├── bbb.out
└── scores.txt

The above is the internal structure of a typical test point compressed file, where scores.txt records all test points and their corresponding scores and test point prompts. For example:

aaa 10 #asdad#
bbb 10 ##

Each line corresponds to a test point, and the data of the test point is separated by spaces. The first section is the name of the test point, the second section is the score corresponding to the test point, and the third section is the prompt information of the test point.
These test points are reflected in other files except scores.txt . Each test point corresponds to two files named after the test point, one with the suffix of .in and the other with the suffix of .out . Corresponding to the input and output of the test point respectively.
After understanding the internal structure of the compressed package, let's turn our attention back to analyzeZipthe function . jszip.loadAsync(file.raw)After calling , we write subsequent code in the .then of the api. We name the parameter of the callback zip , and first get all the files in the ziplet files = Object.keys(zip.files) , . Next we read scores.txt to determine whether all test points are included in the compressed file.

let allIn = true; // 默认所有测试点文件都存在
let points = []; // 初始化测试点数据数组
zip.file("scores.txt")
	.async("string")
	.then((text) => {
    
     // 以 string 格式读取 scores.txt
		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; // 一旦有测试点对应的 in / out 文件不存在压缩包中,将 allIn 赋为 false
			}
		}
		/**
		 * 后续代码
		 */
	})

When allInis true, we can parse the test point file in subsequent code and prepare to send HTTP requests to the backend.

/**
 * 后续代码如下
 */
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",
	});
}

Manually enter individual test points

After reading the above code, it is easy to understand the code for manually uploading a single test point.

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,
				});
			}
		}
	});
},

download test point

Teachers can also download the test point data corresponding to the question in the form of a compressed package through this interface.
code show as below:

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但是依然不支持download
	if ("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); // 释放url
		document.body.removeChild(link); // 释放标签
	} else {
    
    
		// 其他浏览器
		navigator.msSaveBlob(blob, fileName);
	}
},

Guess you like

Origin blog.csdn.net/qq_53126706/article/details/121407378