feat(M5-P1): 教师端发布链路切换 V2
- /list 挂载 TeacherHomeworks(已发布+草稿箱双 Tab,PRD 页 2/3), HomeworkList 仅保留 /homeworklist(全部作业,V1 冻结接口) - NewHomework 切 V2:班级树改 teacherApi.classTree(),创建/编辑一律先落草稿 再按需 publish/schedule,附件改 AttachmentUpload(AttachmentItem[]), 「仅查阅」与其余提交方式互斥;转发场景保留 V1 GetDetail(V2 详情有归属校验) - HomeworkList 摘除伪接口 remind 调用(后端无 /api/homework/Remind) - 路由:/accept 重定向 /stu/today,新增 /stu/detail(学生详情页载体), /stu/redo initTab 改 todo(V2 无独立 redo 桶,RETURNED 归 todo)
This commit is contained in:
+21
-21
@@ -5,30 +5,23 @@ import type { RouteRecordRaw } from "vue-router";
|
||||
* 页面间参数通过 query 传递:
|
||||
* - /list/new?serid= (编辑)
|
||||
* - /list/new?fid= (转发)
|
||||
* - /list/new?tempid=(模板引用)
|
||||
* - /list/detail?serid=&type=、/homeworklist/detail?serid=&type=
|
||||
* type: 0=编辑视角(不标读) 1=普通查看(标读)
|
||||
* - /list/detail?serid=、/homeworklist/detail?serid=
|
||||
*
|
||||
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据):
|
||||
* 1=已收到 2=已发送 3=全部作业 4=作业统计
|
||||
* 1=已收到(→学生今日作业) 2=已发送 3=全部作业 4=作业统计
|
||||
* 发作业为「已发送」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮),
|
||||
* 与 UserBase「应用管理>应用类型管理」范式一致。
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// v1 教师默认落地:我布置的
|
||||
{ path: "/", redirect: "/list" },
|
||||
// 学生收件箱(v2 保留,不在菜单)
|
||||
{
|
||||
path: "/accept",
|
||||
name: "homework_accept",
|
||||
component: () => import("@/views/homework/HomeworkAccept.vue"),
|
||||
meta: { title: "已收到", modId: 1 },
|
||||
},
|
||||
// 我布置的(教师端作业列表)
|
||||
// v1 收件箱语义已由学生端列表替代(M5 §2.3):旧地址与书签统一导向学生今日作业
|
||||
{ path: "/accept", redirect: "/stu/today" },
|
||||
// 我布置的(教师端 V2:已发布 + 草稿箱)
|
||||
{
|
||||
path: "/list",
|
||||
name: "homework_list",
|
||||
component: () => import("@/views/homework/HomeworkList.vue"),
|
||||
component: () => import("@/views/homework/TeacherHomeworks.vue"),
|
||||
meta: { title: "我布置的", modId: 2 },
|
||||
},
|
||||
// 提交情况(某作业的已交/未交名单;从列表行或菜单进入,无 serid 时页内选择作业)
|
||||
@@ -53,15 +46,15 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/homework/NewHomework.vue"),
|
||||
meta: { title: "发作业", modId: 22, parentId: 2 },
|
||||
},
|
||||
// 全部作业(管理端)
|
||||
// 全部作业(管理端,V1 冻结接口;详情走 /homeworklist/detail 的 V1 分支)
|
||||
{
|
||||
path: "/homeworklist",
|
||||
name: "homework_manage",
|
||||
component: () => import("@/views/homework/HomeworkList.vue"),
|
||||
meta: { title: "全部作业", modId: 3 },
|
||||
},
|
||||
// 作业详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
|
||||
// 已发送下 /list/detail,全部作业下 /homeworklist/detail;type 区分是否标读)
|
||||
// 作业详情(同一组件挂两个路由:/list/detail 走 V2 详情+统计(仅本人作业),
|
||||
// /homeworklist/detail 走 V1 冻结 Detail(管理端可看他人作业;V2 有 requireOwned 校验)
|
||||
{
|
||||
path: "/list/detail",
|
||||
name: "homework_list_detail",
|
||||
@@ -83,19 +76,26 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
|
||||
/* ---- v2 学生端 ---- */
|
||||
// 学生·今日作业
|
||||
// 学生·今日作业(tab=all/todo/done/closed)
|
||||
{
|
||||
path: "/stu/today",
|
||||
name: "student_today",
|
||||
component: () => import("@/views/homework/StudentHomeworks.vue"),
|
||||
meta: { title: "今日作业", modId: 31, role: "student" },
|
||||
},
|
||||
// 学生·待订正(复用同一页,默认过滤待订正)
|
||||
// 学生·待订正(V2 无独立 redo 桶:RETURNED 归入 todo,页内红色强化展示)
|
||||
{
|
||||
path: "/stu/redo",
|
||||
name: "student_redo",
|
||||
component: () => import("@/views/homework/StudentHomeworks.vue"),
|
||||
meta: { title: "待订正", modId: 32, role: "student", initTab: "redo" },
|
||||
meta: { title: "待订正", modId: 32, role: "student", initTab: "todo" },
|
||||
},
|
||||
// 学生·作业详情/提交/批阅结果(PRD 学生页 2/3 载体)
|
||||
{
|
||||
path: "/stu/detail",
|
||||
name: "student_detail",
|
||||
component: () => import("@/views/homework/HomeworkAccept.vue"),
|
||||
meta: { title: "作业详情", modId: 33, role: "student" },
|
||||
},
|
||||
|
||||
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
||||
@@ -103,10 +103,10 @@ const routes: RouteRecordRaw[] = [
|
||||
{ path: "/newhomework", redirect: "/list/new" },
|
||||
{ path: "/edit", redirect: "/list/new" },
|
||||
{ path: "/manage", redirect: "/homeworklist" },
|
||||
{ path: "/homework", redirect: "/accept" },
|
||||
{ path: "/homework", redirect: "/stu/today" },
|
||||
{ path: "/homework/newhomework", redirect: "/list/new" },
|
||||
{ path: "/homework/edit", redirect: "/list/new" },
|
||||
{ path: "/homework/homework_accept", redirect: "/accept" },
|
||||
{ path: "/homework/homework_accept", redirect: "/stu/today" },
|
||||
{ path: "/homework/homework_list", redirect: "/list" },
|
||||
{ path: "/homework/manage", redirect: "/homeworklist" },
|
||||
{ path: "/homework/homework_details", redirect: "/list/detail" },
|
||||
|
||||
@@ -104,20 +104,6 @@ function viewSubmissions(row: HomeworkVo) {
|
||||
function edit(row: HomeworkVo) {
|
||||
router.push({ path: "/list/new", query: { serid: row.id } });
|
||||
}
|
||||
function remind(row: HomeworkVo) {
|
||||
MyConfirm({
|
||||
title: "催交",
|
||||
msg: `向“ ${row.title} ”的未交学生发送提交提醒?`,
|
||||
async onOk() {
|
||||
try {
|
||||
const r = await homeworkApi.remind(row.id).send();
|
||||
Message.success(`已提醒 ${(r && (r as any).reminded) ?? 0} 位未交学生`);
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "催交失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
function del(row: HomeworkVo) {
|
||||
MyConfirm({
|
||||
title: "删除",
|
||||
@@ -236,7 +222,6 @@ const columns = [
|
||||
const nodes: any[] = [];
|
||||
if (row.hwStatus !== "draft" && (row as any).progress) nodes.push(opBtn(h, "提交情况", "icon-chakan", () => viewSubmissions(row)));
|
||||
if (row.hwStatus === "draft") nodes.push(opBtn(h, "编辑", "icon-edit", () => edit(row)));
|
||||
if (row.hwStatus === "collecting" && row.progress && row.progress.unsubmitted > 0) nodes.push(opBtn(h, "催交", "icon-fasong", () => remind(row)));
|
||||
nodes.push(opBtn(h, "删除", "icon-ashbin", () => del(row)));
|
||||
return nodes;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, shallowRef, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { ref, reactive, shallowRef, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useRequest } from "alova/client";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||
import "@wangeditor/editor/dist/css/style.css";
|
||||
import { homeworkApi, baseApi } from "@/api/module/homework";
|
||||
import type { HomeworkDetailVo, HomeworkRequest, UpFileVo } from "@/types/homework";
|
||||
import HomeworkUpload from "@/components/HomeworkUpload.vue";
|
||||
import { teacherApi } from "@/api/module/teacherHomework";
|
||||
import { homeworkApi } from "@/api/module/homework";
|
||||
import type { HomeworkV2Request, HomeworkV2Vo, AttachmentItem } from "@/types/homeworkV2";
|
||||
import type { UpFileVo } from "@/types/homework";
|
||||
import AttachmentUpload from "@/components/AttachmentUpload.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -32,7 +33,7 @@ const REMIND_OPTS = [
|
||||
const form = reactive({
|
||||
title: "",
|
||||
subject: "",
|
||||
deadline: "",
|
||||
deadline: "" as string | Date,
|
||||
content: "",
|
||||
});
|
||||
const submitTypes = ref<string[]>(["image", "attach"]);
|
||||
@@ -40,26 +41,32 @@ const allClasses = ref(false);
|
||||
const selClasses = ref<string[]>([]);
|
||||
const allowLate = ref(false);
|
||||
const remindBefore = ref<string[]>([]);
|
||||
const fileids = ref<number[]>([]);
|
||||
const editFileList = ref<{ name: string; fileid: number }[]>([]);
|
||||
const attachments = ref<AttachmentItem[]>([]);
|
||||
|
||||
/* 班级下拉多选数据(年级分组) */
|
||||
/* 「仅查阅」与其余方式互斥(后端 SubmitTypes:none=不必提交) */
|
||||
watch(submitTypes, (v) => {
|
||||
if (v.includes("none") && v.length > 1) {
|
||||
submitTypes.value = v[v.length - 1] === "none" ? ["none"] : v.filter((x) => x !== "none");
|
||||
}
|
||||
});
|
||||
|
||||
/* ============ 班级下拉多选(V2 教师班级树,叶子 id=classId) ============ */
|
||||
const classGroups = ref<{ grade: string; items: { value: string; label: string }[] }[]>([]);
|
||||
const allClassIds = ref<string[]>([]);
|
||||
async function loadClasses() {
|
||||
try {
|
||||
const tree: any[] = await baseApi.getClassTree().send();
|
||||
const tree = await teacherApi.classTree().send();
|
||||
const groups: { grade: string; items: { value: string; label: string }[] }[] = [];
|
||||
const ids: string[] = [];
|
||||
(tree || []).forEach((g) => {
|
||||
const items = (g.children || []).map((c: any) => ({ value: String(c.id), label: c.name }));
|
||||
const items = (g.children || []).map((c) => ({ value: String(c.id), label: c.name }));
|
||||
items.forEach((i) => ids.push(i.value));
|
||||
groups.push({ grade: g.name, items });
|
||||
});
|
||||
classGroups.value = groups;
|
||||
allClassIds.value = ids;
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "加载班级树失败");
|
||||
}
|
||||
}
|
||||
function toggleAll(v: boolean) {
|
||||
@@ -109,106 +116,119 @@ function handleCreated(editor: any) {
|
||||
onBeforeUnmount(() => editorRef.value?.destroy());
|
||||
|
||||
/* ============ 定时发布 ============ */
|
||||
const timeModal = reactive({ open: false, date: "" });
|
||||
const timeModal = reactive({ open: false, date: "" as string | Date });
|
||||
const dateOpt = { disabledDate: (d: Date) => d && d.valueOf() < Date.now() - 86400000 };
|
||||
function fmt(d: any) {
|
||||
function fmt(d: any): string {
|
||||
if (!d) return "";
|
||||
const dt = d instanceof Date ? d : new Date(String(d).replace(/-/g, "/"));
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:00`;
|
||||
}
|
||||
|
||||
/* ============ 编辑回显 ============ */
|
||||
/* ============ 编辑回显(V2 详情) / 转发(V1 GetDetail 保留给转发场景) ============ */
|
||||
function fillFromV2(d: HomeworkV2Vo) {
|
||||
form.title = (d.title || "").replace("[修订稿]", "");
|
||||
form.subject = d.subject || "";
|
||||
form.deadline = d.deadlineAt || "";
|
||||
form.content = d.description || "";
|
||||
submitTypes.value = Array.isArray(d.submitType) && d.submitType.length ? [...d.submitType] : ["image"];
|
||||
allowLate.value = d.allowLate === 1;
|
||||
remindBefore.value = [d.remind1d === 1 ? "1d" : "", d.remind1h === 1 ? "1h" : ""].filter(Boolean);
|
||||
const cls = (d.classes || []).map((c) => String(c.classId));
|
||||
if (cls.length && cls.length === allClassIds.value.length && cls.every((c) => allClassIds.value.includes(c))) {
|
||||
allClasses.value = true;
|
||||
selClasses.value = [...allClassIds.value];
|
||||
} else {
|
||||
allClasses.value = false;
|
||||
selClasses.value = cls;
|
||||
}
|
||||
attachments.value = [...(d.attachments || [])];
|
||||
if (d.scheduledAt) timeModal.date = d.scheduledAt;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!(isEdit.value || fid.value > 0)) return;
|
||||
const sourceId = isEdit.value ? serid.value : fid.value;
|
||||
try {
|
||||
const detail: HomeworkDetailVo = await homeworkApi.getHomeworkDetails(sourceId).send();
|
||||
if (isEdit.value) {
|
||||
form.title = (detail.title || "").replace("[修订稿]", "");
|
||||
form.subject = (detail as any).subject || "";
|
||||
form.deadline = (detail as any).deadline || "";
|
||||
submitTypes.value = Array.isArray((detail as any).submitType) && (detail as any).submitType.length ? (detail as any).submitType : ["image"];
|
||||
allowLate.value = !!(detail as any).allowLate;
|
||||
remindBefore.value = Array.isArray((detail as any).remindBefore) ? (detail as any).remindBefore : [];
|
||||
if (detail.tlist === "all" || !Array.isArray(detail.tlist)) {
|
||||
allClasses.value = true;
|
||||
selClasses.value = [...allClassIds.value];
|
||||
} else {
|
||||
allClasses.value = false;
|
||||
selClasses.value = detail.tlist.map((v) => String(v));
|
||||
const d = await teacherApi.detail(serid.value).send();
|
||||
if (d.hwStatus !== "DRAFT_UNPUBLISHED" && d.hwStatus !== "DRAFT_SCHEDULED") {
|
||||
Message.info("仅草稿状态的作业可编辑");
|
||||
}
|
||||
if (detail.DsfsTime) timeModal.date = detail.DsfsTime;
|
||||
fillFromV2(d);
|
||||
} else {
|
||||
form.title = detail.title || "";
|
||||
}
|
||||
form.content = detail.Contents || "";
|
||||
if (Array.isArray(detail.ChaoSend) && detail.ChaoSend.length) {
|
||||
editFileList.value = detail.ChaoSend.map((f) => ({ name: f.filename || f.name, fileid: f.fileid || f.id }));
|
||||
fileids.value = editFileList.value.map((f) => f.fileid);
|
||||
// 转发:拿他人作业作底稿(V2 详情有本人归属校验,走 V1 冻结 GetDetail)
|
||||
const d: any = await homeworkApi.getHomeworkDetails(fid.value).send();
|
||||
form.title = d.title || "";
|
||||
form.content = d.Contents || "";
|
||||
attachments.value = (d.ChaoSend || []).map((f: any) => ({
|
||||
fileId: f.fileid || f.id,
|
||||
fileUrl: `/api/file/Preview?id=${f.fileid || f.id}`,
|
||||
fileType: /\.(jpe?g|gif|png|bmp|webp)$/i.test(f.filename || f.name || "") ? "image" : "file",
|
||||
fileName: f.filename || f.name,
|
||||
sort: 0,
|
||||
}));
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "加载作业失败");
|
||||
}
|
||||
});
|
||||
|
||||
/* ============ 提交 ============ */
|
||||
/* ============ 提交(V2:创建/编辑一律先落草稿,再按需 publish / schedule) ============ */
|
||||
const formRef = ref();
|
||||
const isLoading = ref(false);
|
||||
const { send: sendCreate } = useRequest((payload: HomeworkRequest) => homeworkApi.create(payload), { immediate: false });
|
||||
const { send: sendEdit } = useRequest((payload: HomeworkRequest) => homeworkApi.editHomework(payload), { immediate: false });
|
||||
|
||||
async function doAdd(mode: "publish" | "draft" | "time") {
|
||||
if (!(await formRef.value?.validate?.())) {
|
||||
Message.info("请填写带 * 的必填项");
|
||||
return;
|
||||
}
|
||||
if (!allClasses.value && selClasses.value.length === 0) {
|
||||
const classIds = allClasses.value ? [...allClassIds.value] : selClasses.value;
|
||||
if (classIds.length === 0) {
|
||||
Message.info("请选择接收班级");
|
||||
return;
|
||||
}
|
||||
const data: HomeworkRequest = {
|
||||
title: form.title,
|
||||
content: form.content,
|
||||
subject: form.subject,
|
||||
deadline: form.deadline ? fmt(form.deadline) : undefined,
|
||||
submitType: submitTypes.value.length ? submitTypes.value : ["text"],
|
||||
allowLate: allowLate.value,
|
||||
remindBefore: remindBefore.value,
|
||||
receiveList: allClasses.value ? "ALL" : selClasses.value.join(","),
|
||||
receiveType: "class",
|
||||
deptId: "",
|
||||
fileIds: fileids.value.join(","),
|
||||
draft: mode === "draft",
|
||||
};
|
||||
if (mode === "time" && !timeModal.date) {
|
||||
timeModal.open = true;
|
||||
Message.info("请选择定时发布时间");
|
||||
return;
|
||||
}
|
||||
let scheduledAt = "";
|
||||
if (mode === "time") {
|
||||
if (!timeModal.date) {
|
||||
Message.info("请选择定时发布时间");
|
||||
return;
|
||||
}
|
||||
const t = new Date(String(timeModal.date).replace(/-/g, "/")).getTime();
|
||||
if (t < Date.now()) {
|
||||
scheduledAt = fmt(timeModal.date);
|
||||
if (new Date(scheduledAt.replace(/-/g, "/")).getTime() <= Date.now()) {
|
||||
Message.info("定时时间需晚于当前时间");
|
||||
return;
|
||||
}
|
||||
data.timingSendTime = fmt(timeModal.date);
|
||||
}
|
||||
const types = submitTypes.value.length ? submitTypes.value : ["text"];
|
||||
const data: HomeworkV2Request = {
|
||||
title: form.title,
|
||||
subject: form.subject,
|
||||
description: form.content,
|
||||
classIds,
|
||||
attachments: attachments.value.map((a, i) => ({ ...a, sort: i })),
|
||||
deadlineAt: form.deadline ? fmt(form.deadline) : undefined,
|
||||
submitType: types as HomeworkV2Request["submitType"],
|
||||
allowLate: allowLate.value ? 1 : 0,
|
||||
remind1d: remindBefore.value.includes("1d") ? 1 : 0,
|
||||
remind1h: remindBefore.value.includes("1h") ? 1 : 0,
|
||||
};
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
if (!isEdit.value) {
|
||||
await sendCreate(data);
|
||||
Message.success(mode === "draft" ? "已存草稿" : mode === "time" ? "已设置定时发布" : "发布成功");
|
||||
timeModal.open = false;
|
||||
router.push("/list");
|
||||
const id = isEdit.value ? serid.value : await teacherApi.create(data).send();
|
||||
if (isEdit.value) await teacherApi.edit(id, data).send();
|
||||
if (mode === "publish") {
|
||||
await teacherApi.publish(id).send();
|
||||
Message.success("发布成功");
|
||||
} else if (mode === "time") {
|
||||
await teacherApi.schedule(id, { scheduledAt }).send();
|
||||
Message.success("已设置定时发布");
|
||||
} else {
|
||||
data.sourceId = serid.value;
|
||||
if (data.title && data.title.indexOf("[修订稿]") === -1) data.title = "[修订稿]" + data.title;
|
||||
await sendEdit(data);
|
||||
Message.success("保存成功");
|
||||
timeModal.open = false;
|
||||
router.push("/list");
|
||||
Message.success("已存草稿");
|
||||
}
|
||||
timeModal.open = false;
|
||||
router.push("/list");
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "保存失败");
|
||||
} finally {
|
||||
@@ -266,7 +286,7 @@ async function doAdd(mode: "publish" | "draft" | "time") {
|
||||
</CheckboxGroup>
|
||||
</FormItem>
|
||||
<FormItem label="添加附件:">
|
||||
<HomeworkUpload v-model:fileids="fileids" :defaultList="editFileList"></HomeworkUpload>
|
||||
<AttachmentUpload v-model="attachments" buttonText="选择附件"></AttachmentUpload>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user