feat(M5): P0 契约层 + 审批/应用设置下线

- types/homeworkV2.ts:V2 全量类型契约(五态/枚举/矩阵/版本/通知)
- teacherHomework/studentHomework/notification 三客户端(28 端点)
- §6 审批功能下线:删 ApprovalList/ApprovalSetting/ApprovalFlowEditor,
  types/api/menu/menuRoute/App/mock 同步清理,DROPPED_MOD_IDS={5,6}
- §7 应用设置下线:删 HomeworkSetting/TypeSetBox/UserCheckInput,
  SettingApi 与 dict/contacts 死代码清理,DROPPED_MOD_IDS 扩 {21}
- 新增 V2 组件:TeacherHomeworks(已发布+草稿箱)、ReviewModal(批阅弹窗)、
  AttachmentUpload(附件上传)——尚未挂路由,页面切换随后续阶段
This commit is contained in:
wanghu
2026-09-16 02:29:32 +08:00
parent d75aab8d07
commit e6639f7925
40 changed files with 7734 additions and 1535 deletions
+109 -6
View File
@@ -4,6 +4,20 @@ import { useRoute } from "vue-router";
import { Message } from "view-ui-plus";
import { homeworkApi } from "@/api/module/homework";
import type { SubmissionsResultVo, SubmissionBrief, HomeworkVo } from "@/types/homework";
import config from "@/assets/config/host.config";
import Utils from "@/utils";
const serviceBase = config.isMock ? "" : config.serviceHost;
function previewUrl(v: number | string) {
const s = String(v);
if (s.startsWith("data:") || s.startsWith("http")) return s;
return `${serviceBase}/api/file/Preview?id=${s}`;
}
const imgPreview = reactive({ open: false, url: "" });
function zoom(url: string) {
imgPreview.url = url;
imgPreview.open = true;
}
const route = useRoute();
const serid = ref<number>(Number(route.query.serid) || 0);
@@ -78,13 +92,25 @@ const stuColumns = [
title: "操作",
width: 110,
align: "center",
render: (h: any, p: any) => h("Button", { type: "text", size: "small", onClick: () => openGrade(p.row as SubmissionBrief) }, () => (p.row.hasGrade ? "再批" : "批改")),
render: (h: any, p: any) => Utils.operatingHandle(h, p.row.hasGrade ? "再批" : "批改", p.row.hasGrade ? "icon-edit" : "icon-chakan", () => openGrade(p.row as SubmissionBrief)),
},
];
/* ============ 内联批改(走 Grade 接口,核心闭环即时批改) ============ */
const gradeModal = reactive({ open: false, subId: 0, studentName: "", mode: "grade", action: "grade", score: "" as string | number, level: "A", comment: "" });
function openGrade(stu: SubmissionBrief) {
const gradeModal = reactive({
open: false,
subId: 0,
studentName: "",
mode: "grade",
action: "grade",
score: "" as string | number,
level: "A",
comment: "",
answerText: "",
answerImages: [] as string[],
loadingAnswer: false,
});
async function openGrade(stu: SubmissionBrief) {
gradeModal.open = true;
gradeModal.subId = stu.subId;
gradeModal.studentName = stu.studentName;
@@ -93,6 +119,18 @@ function openGrade(stu: SubmissionBrief) {
gradeModal.score = stu.score ?? "";
gradeModal.level = stu.level || "A";
gradeModal.comment = "";
gradeModal.answerText = "";
gradeModal.answerImages = [];
gradeModal.loadingAnswer = true;
try {
const d = await homeworkApi.submissionDetail(stu.subId).send();
gradeModal.answerText = d.text || "";
gradeModal.answerImages = (d.images || []).map((v: any) => String(v)).filter(Boolean);
} catch (e: any) {
Message.error(e.message || "加载作答失败");
} finally {
gradeModal.loadingAnswer = false;
}
}
async function doGrade(action: "grade" | "redo" | "excellent") {
const payload: any = { subId: gradeModal.subId, action };
@@ -166,7 +204,20 @@ function onSave() {
</template>
<!-- 批改弹窗 -->
<Modal v-model="gradeModal.open" :title="`批改 · ${gradeModal.studentName}`" width="440px">
<Modal v-model="gradeModal.open" :title="`批改 · ${gradeModal.studentName}`" width="520px">
<div class="ans-box">
<div class="ans-h">学生作答</div>
<Spin v-if="gradeModal.loadingAnswer" />
<template v-else>
<div class="ans-text">{{ gradeModal.answerText || "(无文字作答)" }}</div>
<div class="ans-imgs" v-if="gradeModal.answerImages.length">
<div class="ans-img" v-for="(u, ui) in gradeModal.answerImages" :key="ui" @click="zoom(previewUrl(u))">
<img :src="previewUrl(u)" />
</div>
</div>
</template>
</div>
<Divider style="margin: 8px 0" />
<Form :label-width="72">
<FormItem label="方式">
<RadioGroup v-model="gradeModal.mode">
@@ -192,15 +243,67 @@ function onSave() {
</Form>
<template #footer>
<Button @click="gradeModal.open = false">取消</Button>
<Button type="warning" style="margin-left: 8px" @click="doGrade('excellent')">标为优秀</Button>
<Button type="primary" :loading="loading" @click="onSave">保存</Button>
<Button type="warning" icon="md-star" style="margin-left: 8px" @click="doGrade('excellent')">标为优秀</Button>
<Button type="primary" icon="md-checkmark" :loading="loading" @click="onSave">保存</Button>
</template>
</Modal>
<!-- 作答图片放大预览 -->
<Modal v-model="imgPreview.open" footer-hide title="作答图片" width="680px">
<div class="zoom-wrap"><img :src="imgPreview.url" /></div>
</Modal>
</div>
</div>
</template>
<style lang="scss" scoped>
.ans-box {
.ans-h {
font-weight: 600;
color: #17233d;
margin-bottom: 6px;
}
.ans-text {
color: #515a6e;
line-height: 1.6;
background: #fafcff;
border: 1px solid #e8eaec;
border-radius: 4px;
padding: 8px 10px;
max-height: 140px;
overflow-y: auto;
}
.ans-imgs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
.ans-img {
width: 80px;
height: 80px;
border: 1px solid #e8eaec;
border-radius: 4px;
overflow: hidden;
cursor: zoom-in;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
&:hover {
border-color: #2d8cf0;
}
}
}
}
.zoom-wrap {
text-align: center;
img {
max-width: 100%;
max-height: 62vh;
}
}
.tip {
color: #808695;
padding: 20px 0;