feat: 提交课程作业前端工程源码(Vue3 + Vite + view-ui-plus)
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { teacherApi } from "@/api/module/teacherHomework";
|
||||
import type { HomeworkV2Vo, StudentMatrixVo } from "@/types/homeworkV2";
|
||||
import ReviewWorkbench from "@/components/ReviewWorkbench.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const serid = ref<number>(Number(route.query.serid) || 0);
|
||||
|
||||
/* ============ 待批作业(已发布且 pendingGrade>0,按待批数降序) ============ */
|
||||
const homeworks = ref<HomeworkV2Vo[]>([]);
|
||||
const loadingHw = ref(false);
|
||||
async function loadHomeworks() {
|
||||
loadingHw.value = true;
|
||||
try {
|
||||
const r = await teacherApi.published({ pageIndex: 1, pageSize: 100 }).send();
|
||||
homeworks.value = (r.content || [])
|
||||
.filter((h) => (h.progress?.pendingGrade ?? 0) > 0)
|
||||
.sort((a, b) => (b.progress?.pendingGrade ?? 0) - (a.progress?.pendingGrade ?? 0));
|
||||
// 未带 serid 进入时自动选中第一份待批作业
|
||||
if (!serid.value && homeworks.value.length) {
|
||||
pickHw(homeworks.value[0].id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "加载待批作业失败");
|
||||
} finally {
|
||||
loadingHw.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ 待批队列(矩阵 pendingGrade=true) ============ */
|
||||
const queue = ref<StudentMatrixVo[]>([]);
|
||||
const idx = ref(0);
|
||||
const loadingQueue = ref(false);
|
||||
const homeworkTitle = ref("");
|
||||
const current = computed(() => queue.value[idx.value] || null);
|
||||
|
||||
async function loadQueue() {
|
||||
if (!serid.value) {
|
||||
queue.value = [];
|
||||
homeworkTitle.value = "";
|
||||
return;
|
||||
}
|
||||
loadingQueue.value = true;
|
||||
try {
|
||||
const hw = homeworks.value.find((h) => h.id === serid.value);
|
||||
homeworkTitle.value = hw ? hw.title : "";
|
||||
const r = await teacherApi.students(serid.value, { pendingGrade: true, pageIndex: 1, pageSize: 200 }).send();
|
||||
queue.value = r.content || [];
|
||||
idx.value = 0;
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "加载待批名单失败");
|
||||
queue.value = [];
|
||||
} finally {
|
||||
loadingQueue.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 批阅/打回完成:移出队列并前移;全批完则刷新作业列表 */
|
||||
function onSaved() {
|
||||
const done = idx.value;
|
||||
queue.value.splice(done, 1);
|
||||
if (queue.value.length === 0) {
|
||||
if (homeworks.value.length <= 1) {
|
||||
homeworks.value = [];
|
||||
serid.value = 0;
|
||||
router.replace({ path: "/grade" });
|
||||
return;
|
||||
}
|
||||
loadHomeworks();
|
||||
return;
|
||||
}
|
||||
idx.value = Math.min(done, queue.value.length - 1);
|
||||
}
|
||||
function select(i: number) {
|
||||
if (i < 0 || i >= queue.value.length) return;
|
||||
idx.value = i;
|
||||
}
|
||||
/** 强制工作台重新加载(同一学生打回后重交再进队列) */
|
||||
const workbenchKey = computed(() => `${serid.value}-${current.value?.studentId ?? ""}-${idx.value}`);
|
||||
|
||||
function pickHw(v: number) {
|
||||
serid.value = v;
|
||||
router.replace({ path: "/grade", query: v ? { serid: String(v) } : {} });
|
||||
loadQueue();
|
||||
}
|
||||
watch(
|
||||
() => route.query.serid,
|
||||
(v) => {
|
||||
const n = Number(v) || 0;
|
||||
if (n !== serid.value) {
|
||||
serid.value = n;
|
||||
loadQueue();
|
||||
}
|
||||
}
|
||||
);
|
||||
onMounted(async () => {
|
||||
await loadHomeworks();
|
||||
loadQueue();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 顶部:待批作业选择 -->
|
||||
<div class="fix mb-12">
|
||||
<div class="fl" style="display: flex; align-items: center; gap: 8px">
|
||||
<span style="color: #515a6e">待批作业:</span>
|
||||
<Select :model-value="serid" style="width: 380px" placeholder="选择一份有待批的作业" filterable :loading="loadingHw" @on-change="pickHw">
|
||||
<Option v-for="hw in homeworks" :key="hw.id" :value="hw.id">
|
||||
{{ hw.subject ? hw.subject + " · " : "" }}{{ hw.title }}(待批 {{ hw.progress!.pendingGrade }})
|
||||
</Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!homeworks.length" class="tip">🎉 当前没有待批改的作业。</p>
|
||||
<p v-else-if="!serid" class="tip">请从上方选择一份待批改的作业。</p>
|
||||
|
||||
<div v-else class="workbench fix">
|
||||
<!-- 左:待批队列 -->
|
||||
<div class="wb-left fl">
|
||||
<div class="wb-h">待批队列({{ queue.length }} 份)</div>
|
||||
<p v-if="!loadingQueue && !queue.length" class="done">🎉 该作业已全部批改完成</p>
|
||||
<Spin v-if="loadingQueue" fix />
|
||||
<ul class="qlist">
|
||||
<li v-for="(s, i) in queue" :key="s.studentId" :class="{ act: i === idx }" @click="select(i)">
|
||||
<div class="q1">
|
||||
{{ s.studentName }}
|
||||
<span v-if="s.isLate" class="q-late">逾期</span>
|
||||
</div>
|
||||
<div class="q2">{{ s.className }} · {{ s.submittedAt ? String(s.submittedAt).slice(5, 16) : "" }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右:提交内容 + 批阅(版本切换/批阅/打回共用工作台) -->
|
||||
<div class="wb-right fr">
|
||||
<div class="wb-h">
|
||||
{{ homeworkTitle }}
|
||||
<span class="pos" v-if="current">第 {{ idx + 1 }} / {{ queue.length }} 份 · {{ current.studentName }}({{ current.className }})</span>
|
||||
</div>
|
||||
<div class="wb-panel" v-if="current">
|
||||
<ReviewWorkbench :key="workbenchKey" :homeworkId="serid" :student="current" @saved="onSaved" />
|
||||
</div>
|
||||
<p v-else class="tip">该作业暂无待批改提交。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tip {
|
||||
color: #808695;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.done {
|
||||
color: #19be6b;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
.workbench {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.wb-h {
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
.pos {
|
||||
font-weight: 400;
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.wb-left {
|
||||
width: 240px;
|
||||
flex: none;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px 10px;
|
||||
position: relative;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
.qlist {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
.q1 {
|
||||
color: #17233d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.q-late {
|
||||
background: #ff9900;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.q2 {
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
&.act {
|
||||
background: #e8f3ff;
|
||||
}
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.wb-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
padding: 0 14px 14px;
|
||||
position: relative;
|
||||
.wb-panel {
|
||||
padding-top: 6px;
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user