修复了成绩查询界面当无数据时显示错误的bug、基本完成了上传附件的功能、调整了报告提交界面的部分查询逻辑。

写了一个暂时无用的下载方法,可以后续查看是否使用。
上传文件方法部分写的后端接口貌似支持在本地文件(AUTO-SCORE)自动生成一个upload文件并上传相应文件,可以后续调整下载位置。
This commit is contained in:
musteven 2025-04-13 22:00:19 +08:00
parent dca09393fe
commit 9afac81d92
10 changed files with 307 additions and 70 deletions

View File

@ -34,6 +34,21 @@ export default {
getPostProblemInfo:(config) => request.get('/problem/getPostProblemInfo', config),//获取当前报告布置信息
findProblemById1:(config)=>request.get('/problem/findProblemById1',config)//获取当前报告具体信息
findProblemById1:(config)=>request.get('/problem/findProblemById1',config),//获取当前报告具体信息
getSubmitReport:(config) => request.get('/problem/getSubmitReport', config),//获取学生已上传报告信息
//文件处理部分
uploadFiles: (data) =>
request({
url: '/importClassStudentsFile/uploadFiles',
method: 'post',
data: data,
}), //学生上传文件
downloadFile: (fileName) =>
request({
url: `/downloadFile/downloadFile/${fileName}`,
method: 'get',
}), // 文件下载
}

View File

@ -12,29 +12,59 @@
<div slot="header" class="clearfix card-header">
<span class="subtitle">实验提交</span>
</div>
<el-input
type="textarea"
:rows="4"
placeholder="请输入你的实验报告"
v-model="reportText"
class="submit-textarea"
></el-input>
<el-button size="small" type="success">上传附件</el-button>
</el-card>
<br>
<el-button size="small" type="success" @click="showUploadDialog">上传附件</el-button>
<div v-if="fileUrls.length > 0" class="file-list">
<span class="file-list-title">已上传文件:</span>
<ul>
<li v-for="(file, index) in fileUrls" :key="index">
{{ file.name }}
</li>
</ul>
</div>
</el-card>
<!-- 文件上传弹窗 -->
<el-dialog :title="'上传附件'" v-model="uploadDialogVisible" width="60%">
<el-upload
ref="uploadComponent"
:auto-upload="false"
:on-change="handleFileChange"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
v-model:file-list="fileList"
drag
accept=".txt,.doc,.docx,.pdf"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
<span slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="uploadFile">确定</el-button>
</span>
</el-dialog>
<ReportActions class="bottom" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import api from '@/api/user';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import ReportActions from "@/views/user/report-submit/components/report-actions.vue";
const route = useRoute();
const ProblemId = route.params.examId;
const description = ref(""); // 实验描述
const reportText = ref(""); // 实验报告文本
const uploadDialogVisible = ref(false); // 控制上传弹窗的显示
const fileList = ref([]); // 上传的文件列表
const fileUrls = ref([]); // 文件的URL列表
const uploadComponent = ref(null); // 用于引用el-upload组件
const getProblemInfo = async () => {
// 查询作业信息
@ -46,12 +76,79 @@ const getProblemInfo = async () => {
}
};
// 显示上传弹窗
const showUploadDialog = () => {
uploadDialogVisible.value = true;
};
// 处理文件选择变化
const handleFileChange = (file, fileList) => {
console.log('File list updated:', fileList);
};
// 处理文件上传成功
const handleUploadSuccess = (response, file, fileList) => {
if (response.success) {
ElMessage.success('文件上传成功');
// 更新文件URL列表
fileUrls.value.push({ url: response.urls, name: response.data });
uploadDialogVisible.value = false;
fileList.value = [];
} else {
ElMessage.error('文件上传失败');
}
};
// 处理文件上传失败
const handleUploadError = (err, file, fileList) => {
console.error("Failed to upload files:", err);
ElMessage.error('文件上传失败');
};
// 上传文件
const uploadFile = async () => {
if (fileList.value.length === 0) {
ElMessage.error('请选择要上传的文件');
return;
}
const formData = new FormData();
fileList.value.forEach(file => {
formData.append('files', file.raw); // 添加文件到FormData
});
try {
const response = await api.uploadFiles(formData); // 调用上传文件的方法
console.log('Response from server:', response); // 打印响应内容
if (response.code === 0) {
ElMessage.success('文件上传成功');
// 更新文件URL列表
fileUrls.value.push({ url: response.urls, name: response.data });
uploadDialogVisible.value = false;
fileList.value = [];
} else {
ElMessage.error('文件上传失败1');
}
} catch (error) {
console.error("Failed to upload files:", error);
ElMessage.error('文件上传失败2');
}
};
// 下载文件
const downloadFile = (url) => {
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.download = url.split('/').pop(); // 提取文件名
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 生命周期钩子
onMounted(() => {
getProblemInfo(); // 挂载时查询作业信息
});
</script>
<style scoped>
@ -84,5 +181,25 @@ onMounted(() => {
width: 100%;
}
.el-upload-dragger {
width: 100%;
height: 200px;
}
.file-list {
margin-top: 20px;
}
.file-list-title {
font-weight: bold;
}
.file-list ul {
list-style-type: none;
padding: 0;
}
.file-list li {
margin: 5px 0;
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<div class="button-container">
<n-button type="primary" size="medium" @click="onSave">保存</n-button>
<n-button type="success" size="medium" @click="onSaveAndSubmit">保存并提交</n-button>
<n-button type="warning" size="medium" @click="onWithdraw">申请撤回</n-button>
<n-button type="primary" size="small" @click="onSave">保存</n-button>
<n-button type="success" size="small" @click="onSaveAndSubmit">保存并提交</n-button>
<n-button type="warning" size="small" @click="onWithdraw">申请撤回</n-button>
</div>
</template>

View File

@ -27,7 +27,7 @@ const plagiarismStatus = ref(0); // 初始化抄袭状态
const getJudgeInfo = async () => {
// 查询批阅相关信息
try {
const res = await api.getStudentRank({ params: { examId } });
const res = await api.getSubmitReport({ params: { examId } });
comment.value = res.data.comment; // 更新教师评语
plagiarismStatus.value = res.data.plagiarismStatus; // 更新抄袭状态
} catch (error) {

View File

@ -14,21 +14,20 @@
<Rightparts class="rightparts" />
</div>
<ReportActions class="bottom" />
</CommonPage>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import api from '@/api/user';
import {useRoute} from 'vue-router';
import ReportActions from "@/views/user/report-submit/components/report-actions.vue";
import Leftparts from "@/views/user/report-submit/components/leftparts.vue";
import Rightparts from "@/views/user/report-submit/components/rightparts.vue";
const route = useRoute()
const examId = route.params.examId;
const ProblemId = route.params.examId;
const classId = route.params.classId;
const loading = ref(true);
const title = ref(""); // 从API获取的作业名称
@ -36,36 +35,17 @@ const countdown = ref(""); // 用于显示倒计时
// 将结束时间转换为Date对象
let endTime = null; // 结束时间初始化为null
const paginationReactive = reactive({
page: 1,
pageSize: 50,
showSizePicker: true,
pageSizes: [50, 100, 150],
itemCount: 0,
prefix: () => {
return '查询出 ' + total + '条记录';
},
onChange: (page) => {
paginationReactive.page = page;
queryExamScores()
},
onUpdatePageSize: (pageSize) => {
paginationReactive.pageSize = pageSize;
paginationReactive.page = 1;
queryExamScores()
}
});
const queryExamScores = async () => {
//查询作业名称信息(直接复用查询排名)
try {
//查询作业名称信息(直接复用查询排名)
const res1 = await api.getStudentRank({ params: { examId } });
//查询作业时间等信息
const res = await api.getPostProblemInfo({ params: { classId, examId } });
const queryExamName = async () => {
//查询作业名称
const res1 = await api.findProblemById1({ params: { ProblemId } });
title.value = res1.data.title; // 更新作业名称
};
const getProblemTime = async () => {
//查询作业时间等信息
try {
const res = await api.getPostProblemInfo({ params: { classId, examId } });
// 将结束时间转换为Date对象
endTime = new Date(res.data.endTime[0], res.data.endTime[1] - 1, res.data.endTime[2], res.data.endTime[3], res.data.endTime[4]);
} catch (error) {
@ -96,7 +76,8 @@ const updateCountdown = () => {
// 使用setInterval来每秒更新倒计时
let intervalId = null;
onMounted(async () => {
await queryExamScores(); // 确保在挂载时查询数据
await queryExamName(); // 确保在挂载时查询数据
await getProblemTime(); // 查询剩余时间
if (endTime) {
updateCountdown(); // 初始更新
intervalId = setInterval(updateCountdown, 1000); // 设置定时器
@ -114,11 +95,6 @@ const rowKey = (rowData) => {
return rowData.id;
};
// 生命周期钩子
onMounted(() => {
queryExamScores(); // 挂载时查询作业名
});
</script>
<style scoped>
@ -160,4 +136,5 @@ onMounted(() => {
width: 100%; /* 宽度占满整个容器 */
}
</style>

View File

@ -20,7 +20,7 @@ const examId = route.params.examId;
const classId = route.params.classId;
const title = ref(""); // 从API获取的作业名称
const score = ref(0); // 个人得分
const rank = ref(0); // 个人排名
const rank = ref("-"); // 个人排名,默认为 "-"
const totalStudents = ref(0); // 班级总人数
const paginationReactive = reactive({
@ -53,18 +53,20 @@ const queryExamScores = async () => {
const res = await api.getStudentRank({
params: { examId }
});
title.value = res.data.title; // 更新作业名称
score.value = res.data.score; // 更新个人得分
rank.value = res.data.rank || "-"; // 更新个人排名,如果rank为空则显示 "-"
};
const queryClassStudents = async () => {
//查询班级总人数
const res1 = await api.getClassStudents({
params: { classId }
});
title.value = res.data.title; // 更新作业名称
score.value = res.data.score; // 更新个人得分
rank.value = res.data.rank; // 更新个人排名
console.log(res1);
totalStudents.value = res1.data;// 更新班级总人数
};
// 行键函数
const rowKey = (rowData) => {
return rowData.id;
@ -73,6 +75,7 @@ const rowKey = (rowData) => {
// 生命周期钩子
onMounted(() => {
queryExamScores(); // 挂载时查询成绩
queryClassStudents();// 同时查询班级人数
});
</script>

View File

@ -359,7 +359,22 @@ public class ProblemController {
if (ureportsubmitted != null) {
return RespBean.ok("查询成绩排名成功!", ureportsubmitted);
} else {
return RespBean.error("查询成绩排名失败!");
return RespBean.error("尚未提交作业!暂时无法查看排名!");
}
}
//根据作业id和学生id获取学生当前报告成绩排名,与上一方法基本类似,处理空搜索时的输出
@GetMapping("/getSubmitReport")
public RespBean getSubmitReport(Integer examId, HttpServletRequest request) {
DecodeToken decodeToken = new DecodeToken(request);
String UserId = decodeToken.getUserId();
int userId = Integer.parseInt(UserId);
UReportSubmitted ureportsubmitted =
problemServiceI.findScoreAndRankByIds(examId, userId);
if (ureportsubmitted != null) {
return RespBean.ok("查询成绩排名成功!", ureportsubmitted);
} else {
return RespBean.ok("尚未提交作业!");
}
}

View File

@ -2,10 +2,15 @@ package com.example.aes.user.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.UrlResource;
import com.example.aes.global.model.RespBean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -13,6 +18,8 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -139,4 +146,25 @@ public class DownLoadFileController {
}
}
// 提供文件下载的方法
@GetMapping("/downloadFile/{fileName:.+}")
public ResponseEntity<javax.annotation.Resource> downloadFile(@PathVariable String fileName) {
try {
String root = System.getProperty("user.dir") + File.separator + "upload";
Path filePath = Paths.get(root).toAbsolutePath().normalize().resolve(fileName);
javax.annotation.Resource resource = (javax.annotation.Resource) new UrlResource(filePath.toUri());
String contentType = "application/octet-stream";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.name() + "\"")
.body(resource);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.badRequest()
.body(null);
}
}
}

View File

@ -1,6 +1,7 @@
package com.example.aes.user.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.UrlResource;
import com.example.aes.global.model.RespBean;
import com.example.aes.user.dao.AdminusersDaoI;
import com.example.aes.user.dao.ClassesDaoI;
@ -11,19 +12,21 @@ import com.example.aes.user.model.PMUser;
import com.example.aes.user.model.Users;
import com.example.aes.user.service.UserServiceI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.*;
@RestController
@ -374,4 +377,79 @@ public class importClassStudentsFileController{
return true;
}
@PostMapping("/uploadFiles") // 学生上传文件并提供其URL
public ResponseEntity<Map<String, Object>> uploadFiles(MultipartFile[] files) {
Map<String, Object> response = new HashMap<>();
try {
// 获取文件存储根目录
String root = System.getProperty("user.dir") + File.separator + "upload";
if (!new File(root).exists()) {
new File(root).mkdirs();
}
// 用于存储所有上传文件的名称和URL
StringBuilder fileNames = new StringBuilder();
StringBuilder fileUrls = new StringBuilder();
for (MultipartFile file : files) {
// 获取文件原始名
String uploadFileName = file.getOriginalFilename();
// 检查文件类型
if (!uploadFileName.endsWith(".txt") && !uploadFileName.endsWith(".doc") &&
!uploadFileName.endsWith(".docx") && !uploadFileName.endsWith(".pdf")) {
throw new IllegalArgumentException("不支持的文件类型: " + uploadFileName);
}
// 获取主要名称
String mainName = FileUtil.mainName(uploadFileName);
// 获取文件后缀名
String extName = FileUtil.extName(uploadFileName);
// 用当前时间避免重复
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
Date date1 = new Date();
String timeStamp = formatter.format(date1);
// 构造新的文件名
String newFileName = mainName + "_" + timeStamp + "." + extName;
// 构造文件保存路径
String filePath = root + File.separator + newFileName;
// 保存文件到服务器
File dest = new File(filePath);
file.transferTo(dest);
// 构造文件访问URL
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/downloadFile/")
.path(newFileName)
.toUriString();
// 添加文件名和URL到返回信息中
fileNames.append(newFileName).append(", ");
fileUrls.append(fileDownloadUri).append(", ");
}
// 移除最后一个逗号和空格
if (fileNames.length() > 0) {
fileNames.setLength(fileNames.length() - 2);
}
if (fileUrls.length() > 0) {
fileUrls.setLength(fileUrls.length() - 2);
}
response.put("code", 0);
response.put("message", "文件上传成功");
response.put("data", fileNames.toString());
response.put("urls", fileUrls.toString());
return ResponseEntity.ok(response);
} catch (Exception e) {
e.printStackTrace();
response.put("code", 1);
response.put("message", "文件上传异常: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
}

View File

@ -0,0 +1,4 @@
智能手机在当代社会的作用不可小觑,它不仅使得人们的通讯变得极为便捷,还提供了多样化的娱乐和工作功能。在我国,智能手机的普及率不断上升,几乎成了每个人生活中的必需品。它的多功能性和便携性,使其成为了现代科技的一个标志性产物。无论是通讯、娱乐还是学习,都离不开这款电子设备。
在城市边缘的那所著名大学的操场上,一场几千人参加的批斗会已经进行了近两个小时。
“爱因斯坦是反动的学术权威,他有奶便是娘,跑去为美帝国主义造原子弹!要建立起革命的科学,就要打倒以相对论为代表的资产阶级理论黑旗!”
然后在法文词下列出同义的上海方言词语,再尽可能详尽地列出近义的上海方言词语、短语或句子。这种编写方式,对中法两国学习者或词典使用者尽可能多地掌握法汉词汇,显然是很有意义的。