feat: 前端增加提交功能

This commit is contained in:
SikongJueluo 2025-08-19 21:02:49 +08:00
parent 2aef180ddb
commit ca0322137b
No known key found for this signature in database
6 changed files with 236 additions and 101 deletions

View File

@ -1,6 +1,7 @@
using DotNext; using DotNext;
using LinqToDB; using LinqToDB;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Tapper;
namespace Database; namespace Database;
@ -231,6 +232,7 @@ public class Exam
/// <summary> /// <summary>
/// 资源类型枚举 /// 资源类型枚举
/// </summary> /// </summary>
[TranspilationSource]
public static class ResourceTypes public static class ResourceTypes
{ {
/// <summary> /// <summary>

View File

@ -0,0 +1,117 @@
<script setup lang="ts">
import { templateRef } from "@vueuse/core";
import { File, UploadIcon, XIcon } from "lucide-vue-next";
import { isNull } from "mathjs";
import { useSlots } from "vue";
const slots = useSlots();
interface Props {
autoUpload?: boolean;
closeAfterUpload?: boolean;
callback: (files: File[]) => void;
}
const props = withDefaults(defineProps<Props>(), {
autoUpload: false,
closeAfterUpload: false,
});
const inputFiles = defineModel<File[] | null>("inputFiles", { default: null });
const isShowModal = defineModel<boolean>("isShowModal", { default: false });
const fileInputRef = templateRef("fileInputRef");
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (!files) return;
inputFiles.value = Array.from(files);
if (props.autoUpload) handleUpload();
}
function handleFileDrop(event: DragEvent) {
const files = event.dataTransfer?.files;
if (!files) return;
inputFiles.value = Array.from(files);
if (props.autoUpload) handleUpload();
}
function handleUpload() {
if (!inputFiles.value) return;
props.callback(inputFiles.value);
if (props.closeAfterUpload) close();
}
function show() {
isShowModal.value = true;
}
function close() {
isShowModal.value = false;
}
defineExpose({
show,
close,
});
</script>
<template>
<div v-if="isShowModal" class="modal modal-open overflow-hidden">
<div class="modal-box overflow-hidden flex flex-col gap-3">
<div
class="flex justify-between items-center pb-3 border-b border-base-300"
>
<h2 class="text-2xl font-bold text-base-content">文件上传</h2>
<button @click="close" class="btn btn-sm btn-circle btn-ghost">
<XIcon class="w-6 h-6" />
</button>
</div>
<div
class="border-2 border-dashed border-base-300 rounded-lg text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-4/2 flex items-center justify-center"
@click="fileInputRef.click()"
@dragover.prevent
@dragenter.prevent
@drop.prevent="handleFileDrop"
>
<div v-if="slots.content">
<slot name="content"></slot>
</div>
<div v-else class="flex flex-col items-center gap-3">
<File class="w-12 h-12 text-base-content opacity-40" />
<div class="text-sm text-base-content/70 text-center">
<div class="font-medium mb-1">点击或拖拽上传</div>
</div>
</div>
<div v-else class="flex flex-col items-center gap-2">
<File class="w-8 h-8 text-success" />
<div class="text-xs font-medium text-success text-center">
{{ inputFiles?.[0]?.name }}
</div>
<div class="text-xs text-base-content/50">点击重新选择</div>
</div>
</div>
<input
type="file"
ref="fileInputRef"
@change="handleFileChange"
accept=""
class="hidden"
/>
<button
v-if="!autoUpload"
class="btn btn-primary btn-sm w-full h-10"
@click="handleUpload"
:disabled="isNull(inputFiles) || inputFiles.length === 0"
>
<UploadIcon class="w-6 h-6" />
上传
</button>
</div>
<div class="modal-backdrop" @click="close"></div>
</div>
</template>
<style lang="postcss" scoped></style>

View File

@ -14,9 +14,8 @@ const router = createRouter({
{ path: "/login", name: "login", component: AuthView }, { path: "/login", name: "login", component: AuthView },
{ path: "/project", name: "project", component: ProjectView }, { path: "/project", name: "project", component: ProjectView },
{ path: "/test", name: "test", component: TestView }, { path: "/test", name: "test", component: TestView },
{ path: "/user/:page?", name: "user", component: UserView }, { path: "/user/:page*", name: "user", component: UserView },
{ path: "/exam/:examId?", name: "exam", component: ExamView }, { path: "/exam/:page*", name: "exam", component: ExamView },
{ path: "/exam", redirect: "/exam/" },
{ path: "/markdown", name: "markdown", component: MarkdownEditor }, { path: "/markdown", name: "markdown", component: MarkdownEditor },
], ],
}); });

View File

@ -8,19 +8,7 @@
{{ mode === "create" ? "新建实验" : "编辑实验" }} {{ mode === "create" ? "新建实验" : "编辑实验" }}
</h2> </h2>
<button @click="close" class="btn btn-sm btn-circle btn-ghost"> <button @click="close" class="btn btn-sm btn-circle btn-ghost">
<svg <XIcon class="w-6 h-6" />
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
@ -417,11 +405,13 @@ import {
BinaryIcon, BinaryIcon,
FileArchiveIcon, FileArchiveIcon,
FileJsonIcon, FileJsonIcon,
XIcon,
} from "lucide-vue-next"; } from "lucide-vue-next";
import { import {
ExamClient, ExamClient,
ExamDto, ExamDto,
ResourceClient, ResourceClient,
ResourcePurpose,
type FileParameter, type FileParameter,
} from "@/APIClient"; } from "@/APIClient";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
@ -685,7 +675,12 @@ async function uploadExamResources(examId: string) {
data: uploadFiles.value.mdFile, data: uploadFiles.value.mdFile,
fileName: uploadFiles.value.mdFile.name, fileName: uploadFiles.value.mdFile.name,
}; };
await client.addResource("doc", "template", examId, mdFileParam); await client.addResource(
"doc",
ResourcePurpose.Template,
examId,
mdFileParam,
);
console.log("MD文档上传成功"); console.log("MD文档上传成功");
} }
@ -695,7 +690,12 @@ async function uploadExamResources(examId: string) {
data: imageFile, data: imageFile,
fileName: imageFile.name, fileName: imageFile.name,
}; };
await client.addResource("image", "template", examId, imageFileParam); await client.addResource(
"image",
ResourcePurpose.Template,
examId,
imageFileParam,
);
console.log("图片上传成功:", imageFile.name); console.log("图片上传成功:", imageFile.name);
} }
@ -707,7 +707,7 @@ async function uploadExamResources(examId: string) {
}; };
await client.addResource( await client.addResource(
"bitstream", "bitstream",
"template", ResourcePurpose.Template,
examId, examId,
bitstreamFileParam, bitstreamFileParam,
); );
@ -720,7 +720,12 @@ async function uploadExamResources(examId: string) {
data: canvasFile, data: canvasFile,
fileName: canvasFile.name, fileName: canvasFile.name,
}; };
await client.addResource("canvas", "template", examId, canvasFileParam); await client.addResource(
"canvas",
ResourcePurpose.Template,
examId,
canvasFileParam,
);
console.log("画布模板上传成功:", canvasFile.name); console.log("画布模板上传成功:", canvasFile.name);
} }
@ -732,7 +737,7 @@ async function uploadExamResources(examId: string) {
}; };
await client.addResource( await client.addResource(
"resource", "resource",
"template", ResourcePurpose.Template,
examId, examId,
resourceFileParam, resourceFileParam,
); );
@ -775,6 +780,7 @@ defineExpose({
show, show,
close, close,
editExam, editExam,
editExamInfo,
}); });
</script> </script>

View File

@ -13,19 +13,7 @@
@click="closeExamDetail" @click="closeExamDetail"
class="btn btn-sm btn-circle btn-ghost" class="btn btn-sm btn-circle btn-ghost"
> >
<svg <XIcon class="w-6 h-6" />
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
@ -147,17 +135,18 @@
<div class="space-y-4"> <div class="space-y-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-base-content/70">当前状态</span> <span class="text-base-content/70">当前状态</span>
<div class="badge badge-error">未完成</div> <div class="badge badge-error">
</div> {{
isUndefined(commitsList) || commitsList.length === 0
<div class="flex justify-between items-center"> ? "未提交"
<span class="text-base-content/70">批阅状态</span> : "已提交"
<div class="badge badge-ghost">待提交</div> }}
</div>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-base-content/70">成绩</span> <span class="text-base-content/70">成绩</span>
<span class="text-base-content/50">未评分</span> <div class="badge badge-ghost">未评分</div>
</div> </div>
</div> </div>
@ -167,17 +156,22 @@
<div class="space-y-3"> <div class="space-y-3">
<h4 class="font-medium text-base-content">提交历史</h4> <h4 class="font-medium text-base-content">提交历史</h4>
<div <div
v-if="isUndefined(commitsList)" v-if="isUndefined(commitsList) || commitsList.length === 0"
class="text-sm text-base-content/50 text-center py-4" class="text-sm text-base-content/50 text-center py-4"
> >
暂无提交记录 暂无提交记录
</div> </div>
<div v-else class="overflow-y-auto"> <div v-else class="overflow-y-auto">
<ul class="steps steps-vertical"> <ul class="steps steps-vertical">
<li class="step step-primary">Register</li> <li
<li class="step step-primary">Choose plan</li> class="step"
<li class="step">Purchase</li> :class="{ 'step-primary': _idx === 1 }"
<li class="step">Receive Product</li> v-for="(commit, _idx) in commitsList.slice(0, 3)"
>
{{ commit.id }} ---
{{ commit.uploadTime.toDateString() }}
</li>
<li class="step" v-if="commitsList.length > 3">......</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -187,58 +181,27 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="space-y-3"> <div class="space-y-3">
<button @click="startExam" class="btn btn-primary w-full"> <button @click="startExam" class="btn btn-primary w-full">
<svg <Smile class="w-5 h-5" />
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
开始实验 开始实验
</button> </button>
<button @click="uploadModal?.show" class="btn btn-info w-full">
<Upload class="w-5 h-5" />
提交实验
</button>
<button <button
@click="downloadResources" @click="downloadResources"
class="btn btn-outline w-full" class="btn btn-outline w-full"
:disabled="downloadingResources" :disabled="downloadingResources"
> >
<svg <Download class="w-5 h-5" />
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span v-if="downloadingResources">下载中...</span> <span v-if="downloadingResources">下载中...</span>
<span v-else>下载资源包</span> <span v-else>下载资源包</span>
</button> </button>
<button class="btn btn-outline w-full"> <button class="btn btn-outline w-full">
<svg <GitGraph class="w-5 h-5" />
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
查看记录 查看记录
</button> </button>
</div> </div>
@ -247,6 +210,12 @@
</div> </div>
</div> </div>
<div class="modal-backdrop" @click="closeExamDetail"></div> <div class="modal-backdrop" @click="closeExamDetail"></div>
<UploadModal
ref="uploadModal"
class="fixed z-auto"
:auto-upload="true"
:callback="submitExam"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -266,10 +235,16 @@ import { formatDate } from "@/utils/Common";
import { computed } from "vue"; import { computed } from "vue";
import { watch } from "vue"; import { watch } from "vue";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
import { Download, GitGraph, Smile, Upload, XIcon } from "lucide-vue-next";
import UploadModal from "@/components/UploadModal.vue";
import { templateRef } from "@vueuse/core";
import { toFileParameter } from "@/utils/Common";
const alertStore = useRequiredInjection(useAlertStore); const alertStore = useRequiredInjection(useAlertStore);
const router = useRouter(); const router = useRouter();
const uploadModal = templateRef("uploadModal");
const show = defineModel<boolean>("show", { const show = defineModel<boolean>("show", {
default: false, default: false,
}); });
@ -350,6 +325,24 @@ const startExam = () => {
} }
}; };
const submitExam = (files: File[]) => {
try {
const client = AuthManager.createClient(ResourceClient);
for (const file of files) {
client.addResource(
"compression",
ResourcePurpose.Homework,
props.selectedExam.id,
toFileParameter(file),
);
}
} catch (err: any) {
alertStore.error(err.message || "上传资料失败");
console.error("上传资料失败:", err);
}
};
const closeExamDetail = () => { const closeExamDetail = () => {
show.value = false; show.value = false;
}; };

View File

@ -191,6 +191,7 @@ import { templateRef } from "@vueuse/core";
import { isArray, isNull } from "lodash"; import { isArray, isNull } from "lodash";
import { watch } from "vue"; import { watch } from "vue";
import { watchEffect } from "vue"; import { watchEffect } from "vue";
import { nextTick } from "vue";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -218,13 +219,18 @@ watch(
}, },
); );
watchEffect(() => { watch(
if (showEditModal.value) { () => showEditModal.value,
router.replace({ path: `/exam/edit` }); () => {
} else { if (showEditModal.value) {
router.replace({ path: `/exam` }); router.replace({
} path: `/exam/edit/${examEditModalRef.value?.editExamInfo.id}`,
}); });
} else {
router.replace({ path: `/exam` });
}
},
);
async function refreshExams() { async function refreshExams() {
loading.value = true; loading.value = true;
@ -263,7 +269,8 @@ async function handleCardClicked(event: MouseEvent, examId: string) {
} }
async function handleEditExamClicked(event: MouseEvent, examId: string) { async function handleEditExamClicked(event: MouseEvent, examId: string) {
examEditModalRef?.value?.editExam(examId); await examEditModalRef?.value?.editExam(examId);
router.replace(`/exam/edit/${examId}`);
} }
// //
@ -278,17 +285,28 @@ onMounted(async () => {
await refreshExams(); await refreshExams();
}); });
async function loadBasicPage(page: string) {
if (page === "") return;
else if (page === "edit") showEditModal.value = true;
else if (page) await viewExam(page);
else router.push("/exam");
}
onMounted(async () => { onMounted(async () => {
// examId // examId
const examId = route.params.examId as string; const page = route.params.page;
if (examId === "") return;
if (isArray(examId)) return; if (Array.isArray(page)) {
if (page.length == 1) await loadBasicPage(page[0]);
if (examId === "edit") showEditModal.value = true; else if (page.length == 2) {
if (page[0] === "edit") {
if (examId) { await examEditModalRef.value?.editExam(page[1]);
await viewExam(examId); } else {
router.push("/exam");
}
} else router.push("/exam");
} else {
await loadBasicPage(page);
} }
}); });
</script> </script>