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:
@@ -0,0 +1,522 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, h } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRequest } from "alova/client";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { teacherApi } from "@/api/module/teacherHomework";
|
||||
import type { HomeworkV2Vo, TeacherHomeworkQuery } from "@/types/homeworkV2";
|
||||
import type { BasePageResult } from "@/types/homeworkV2";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import Utils from "@/utils";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const SUBJECTS = ["语文", "数学", "英语", "物理", "化学", "生物", "道德与法治", "历史", "地理", "科学", "综合实践"];
|
||||
|
||||
/* ============ 顶级视图:已发布 / 草稿箱 ============ */
|
||||
type ViewMode = "published" | "drafts";
|
||||
const viewMode = ref<ViewMode>("published");
|
||||
|
||||
/* 已发布状态筛选(空 = 全部) */
|
||||
const PUB_TABS = [
|
||||
{ name: "", label: "全部" },
|
||||
{ name: "PUBLISHED", label: "进行中" },
|
||||
{ name: "CLOSED", label: "已截止" },
|
||||
{ name: "ARCHIVED", label: "已归档" },
|
||||
];
|
||||
const pubStatus = ref("");
|
||||
|
||||
/* ============ 搜索条件 ============ */
|
||||
const keyword = ref("");
|
||||
const subject = ref("");
|
||||
|
||||
/* ============ 分页 ============ */
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
|
||||
|
||||
const EMPTY_PAGE: BasePageResult<HomeworkV2Vo> = { content: [], total: 0, pageIndex: 1, pageSize: 10, totalPages: 0, hasNext: false };
|
||||
const { data: page, loading, send: reload } = useRequest((params: TeacherHomeworkQuery, mode: ViewMode) =>
|
||||
mode === "drafts" ? teacherApi.drafts(params) : teacherApi.published(params),
|
||||
{ immediate: false, initialData: EMPTY_PAGE });
|
||||
|
||||
async function run() {
|
||||
const params: TeacherHomeworkQuery = {
|
||||
keyword: keyword.value || undefined,
|
||||
subject: subject.value || undefined,
|
||||
pageIndex: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (viewMode.value === "published" && pubStatus.value) params.hwStatus = pubStatus.value;
|
||||
try {
|
||||
await reload(params, viewMode.value);
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "获取作业列表失败");
|
||||
}
|
||||
}
|
||||
function doSearch() {
|
||||
pageNum.value = 1;
|
||||
run();
|
||||
}
|
||||
function switchView(name: string) {
|
||||
viewMode.value = name as ViewMode;
|
||||
pageNum.value = 1;
|
||||
run();
|
||||
}
|
||||
function onPubTab(name: string) {
|
||||
pubStatus.value = name;
|
||||
doSearch();
|
||||
}
|
||||
function pageUpdate(p: number) {
|
||||
pageNum.value = p;
|
||||
run();
|
||||
}
|
||||
function pageSizeUpdate(size: number) {
|
||||
pageNum.value = 1;
|
||||
pageSize.value = size;
|
||||
localStorage.setItem("customPageCount", String(size));
|
||||
run();
|
||||
}
|
||||
|
||||
run();
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
() => {
|
||||
pageNum.value = 1;
|
||||
keyword.value = "";
|
||||
subject.value = "";
|
||||
pubStatus.value = "";
|
||||
viewMode.value = "published";
|
||||
run();
|
||||
}
|
||||
);
|
||||
|
||||
/* ============ 状态色 ============ */
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
DRAFT_UNPUBLISHED: "default",
|
||||
DRAFT_SCHEDULED: "blue",
|
||||
PUBLISHED: "primary",
|
||||
CLOSED: "warning",
|
||||
ARCHIVED: "success",
|
||||
};
|
||||
function fmtTime(v?: string | null): string {
|
||||
return v ? String(v).slice(0, 16) : "—";
|
||||
}
|
||||
|
||||
/* ============ 行操作 ============ */
|
||||
function viewDetail(row: HomeworkV2Vo) {
|
||||
router.push({ path: "/list/detail", query: { serid: String(row.id) } });
|
||||
}
|
||||
function viewSubmissions(row: HomeworkV2Vo) {
|
||||
router.push({ path: "/submissions", query: { serid: String(row.id) } });
|
||||
}
|
||||
function toGrade(row: HomeworkV2Vo) {
|
||||
router.push({ path: "/grade", query: { serid: String(row.id) } });
|
||||
}
|
||||
function edit(row: HomeworkV2Vo) {
|
||||
router.push({ path: "/list/new", query: { serid: String(row.id) } });
|
||||
}
|
||||
function remind(row: HomeworkV2Vo, type: "NOT_READ" | "NOT_SUBMITTED") {
|
||||
const label = type === "NOT_READ" ? "未读" : "未交";
|
||||
MyConfirm({
|
||||
title: "提醒",
|
||||
msg: `向“ ${row.title} ”的${label}学生发送提醒?`,
|
||||
async onOk() {
|
||||
try {
|
||||
const r = await teacherApi.remind(row.id, type).send();
|
||||
Message.success(`已提醒 ${r?.reminded ?? 0} 位${label}学生`);
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "提醒失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* 延长截止(仅进行中;新截止须晚于 now 与原截止) */
|
||||
const extendModal = reactive({ open: false, row: null as HomeworkV2Vo | null, time: null as Date | null, saving: false });
|
||||
function openExtend(row: HomeworkV2Vo) {
|
||||
extendModal.row = row;
|
||||
extendModal.time = null;
|
||||
extendModal.open = true;
|
||||
}
|
||||
async function doExtend() {
|
||||
const row = extendModal.row;
|
||||
if (!row) return;
|
||||
if (!extendModal.time) {
|
||||
Message.info("请选择新的截止时间");
|
||||
return;
|
||||
}
|
||||
const d = extendModal.time;
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const at = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:00`;
|
||||
if (row.deadlineAt && at <= String(row.deadlineAt)) {
|
||||
Message.info("新截止时间必须晚于原截止时间");
|
||||
return;
|
||||
}
|
||||
extendModal.saving = true;
|
||||
try {
|
||||
await teacherApi.extend(row.id, at).send();
|
||||
Message.success("截止时间已延长");
|
||||
extendModal.open = false;
|
||||
run();
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "延长截止失败");
|
||||
} finally {
|
||||
extendModal.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyRow(row: HomeworkV2Vo) {
|
||||
MyConfirm({
|
||||
title: "复制",
|
||||
msg: `复制“ ${row.title} ”为新草稿?`,
|
||||
info: "复制作业内容与班级设置,不含提交与阅读数据",
|
||||
async onOk() {
|
||||
try {
|
||||
const newId = await teacherApi.copy(row.id).send();
|
||||
Message.success("已复制为新草稿");
|
||||
router.push({ path: "/list/new", query: { serid: String(newId) } });
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "复制失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
function archive(row: HomeworkV2Vo) {
|
||||
MyConfirm({
|
||||
title: "归档",
|
||||
msg: `归档“ ${row.title} ”?`,
|
||||
info: "归档后作业只读,不再触发任何提醒",
|
||||
async onOk() {
|
||||
try {
|
||||
await teacherApi.archive(row.id).send();
|
||||
Message.success("已归档");
|
||||
run();
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "归档失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
function doExport(row: HomeworkV2Vo) {
|
||||
window.location.href = teacherApi.exportUrl(row.id);
|
||||
}
|
||||
|
||||
/* 草稿箱操作 */
|
||||
function publishDraft(row: HomeworkV2Vo) {
|
||||
MyConfirm({
|
||||
title: "发布",
|
||||
msg: `立即发布“ ${row.title} ”?`,
|
||||
info: "发布后参与班级学生将收到作业通知",
|
||||
async onOk() {
|
||||
try {
|
||||
await teacherApi.publish(row.id).send();
|
||||
Message.success("发布成功");
|
||||
switchView("published");
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "发布失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
function cancelSchedule(row: HomeworkV2Vo) {
|
||||
MyConfirm({
|
||||
title: "取消定时",
|
||||
msg: `取消“ ${row.title} ”的定时发布?`,
|
||||
info: "取消后作业回到草稿状态",
|
||||
async onOk() {
|
||||
try {
|
||||
await teacherApi.schedule(row.id, { cancel: true }).send();
|
||||
Message.success("已取消定时");
|
||||
run();
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "取消定时失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
function del(row: HomeworkV2Vo) {
|
||||
MyConfirm({
|
||||
title: "删除",
|
||||
msg: `确定删除“ ${row.title} ”这条作业吗?`,
|
||||
info: "删除后不可恢复",
|
||||
async onOk() {
|
||||
try {
|
||||
await teacherApi.remove(row.id).send();
|
||||
Message.success("删除成功");
|
||||
if ((page.value?.content?.length || 0) <= 1 && pageNum.value > 1) pageNum.value -= 1;
|
||||
run();
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "删除失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* 文本按钮 */
|
||||
function btn(text: string, onClick: () => void) {
|
||||
return h("Button", { type: "text", size: "small", onClick }, () => text);
|
||||
}
|
||||
|
||||
/* ============ 已发布列表列 ============ */
|
||||
const pubColumns = computed(() => [
|
||||
{ title: "序号", type: "index", width: 60, align: "center" },
|
||||
{
|
||||
title: "标题",
|
||||
key: "title",
|
||||
minWidth: 200,
|
||||
align: "left",
|
||||
tooltip: true,
|
||||
render: (h: any, p: any) =>
|
||||
h("a", { style: "color:#2d8cf0;", onClick: () => viewDetail(p.row) }, () => p.row.title),
|
||||
},
|
||||
{ title: "学科", key: "subject", width: 80, align: "center" },
|
||||
{
|
||||
title: "班级",
|
||||
width: 130,
|
||||
align: "left",
|
||||
tooltip: true,
|
||||
render: (h: any, p: any) => h("span", {}, (p.row.classes || []).map((c: any) => c.className).join("、") || "—"),
|
||||
},
|
||||
{ title: "发布时间", width: 130, align: "left", render: (h: any, p: any) => h("span", {}, fmtTime(p.row.publishedAt)) },
|
||||
{ title: "截止时间", width: 130, align: "left", render: (h: any, p: any) => h("span", {}, fmtTime(p.row.deadlineAt)) },
|
||||
{
|
||||
title: "应交",
|
||||
width: 65,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.should ?? "—")),
|
||||
},
|
||||
{
|
||||
title: "已读",
|
||||
width: 65,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.read ?? "—")),
|
||||
},
|
||||
{
|
||||
title: "已交",
|
||||
width: 65,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.submitted ?? "—")),
|
||||
},
|
||||
{
|
||||
title: "未读未交",
|
||||
width: 85,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => {
|
||||
const g = p.row.progress;
|
||||
if (!g) return h("span", { style: "color:#999;" }, "—");
|
||||
return h("span", { style: g.unreadUndone > 0 ? "color:#ed4014;font-weight:600;" : "color:#999;" }, String(g.unreadUndone));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "提交率",
|
||||
width: 80,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => {
|
||||
const g = p.row.progress;
|
||||
if (!g) return h("span", { style: "color:#999;" }, "—");
|
||||
return h("span", {}, `${g.submitRate}%`);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "待批阅",
|
||||
width: 75,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => {
|
||||
const g = p.row.progress;
|
||||
if (!g) return h("span", { style: "color:#999;" }, "—");
|
||||
return h("span", { style: g.pendingGrade > 0 ? "color:#f90;font-weight:600;" : "color:#999;" }, String(g.pendingGrade));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
width: 90,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => h("Tag", { color: STATUS_COLOR[p.row.hwStatus] || "default" }, () => p.row.hwStatusDesc || p.row.hwStatus),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 250,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
render: (h: any, p: any) => {
|
||||
const row = p.row as HomeworkV2Vo;
|
||||
const nodes: any[] = [];
|
||||
nodes.push(btn("提交情况", () => viewSubmissions(row)));
|
||||
if ((row.progress?.pendingGrade ?? 0) > 0) nodes.push(btn("批阅", () => toGrade(row)));
|
||||
const more: any[] = [];
|
||||
if (row.hwStatus === "PUBLISHED") {
|
||||
if ((row.progress?.unread ?? 0) > 0) more.push(mItem("提醒未读", () => remind(row, "NOT_READ")));
|
||||
if ((row.progress?.unsubmitted ?? 0) > 0) more.push(mItem("催交", () => remind(row, "NOT_SUBMITTED")));
|
||||
more.push(mItem("延长截止", () => openExtend(row)));
|
||||
}
|
||||
more.push(mItem("复制", () => copyRow(row)));
|
||||
if (row.hwStatus === "PUBLISHED" || row.hwStatus === "CLOSED") more.push(mItem("归档", () => archive(row)));
|
||||
more.push(mItem("导出报表", () => doExport(row)));
|
||||
nodes.push(
|
||||
h("Dropdown", { transfer: true, onOnClick: (name: string) => onMore(name) }, () => [
|
||||
h("Button", { type: "text", size: "small" }, () => "更多∨"),
|
||||
h("DropdownMenu", { slot: "list" }, () => more),
|
||||
])
|
||||
);
|
||||
return h("div", {}, nodes);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/* 更多菜单回调(闭包 name→action) */
|
||||
const moreActions = new Map<string, () => void>();
|
||||
let moreSeq = 0;
|
||||
function mItem(text: string, action: () => void) {
|
||||
const key = `m${++moreSeq}`;
|
||||
moreActions.set(key, action);
|
||||
return h("DropdownItem", { name: key }, () => text);
|
||||
}
|
||||
function onMore(name: string) {
|
||||
moreActions.get(name)?.();
|
||||
}
|
||||
|
||||
/* ============ 草稿箱列 ============ */
|
||||
const draftColumns = computed(() => [
|
||||
{ title: "序号", type: "index", width: 60, align: "center" },
|
||||
{ title: "标题", key: "title", minWidth: 220, align: "left", tooltip: true },
|
||||
{ title: "学科", key: "subject", width: 90, align: "center" },
|
||||
{
|
||||
title: "班级",
|
||||
width: 150,
|
||||
align: "left",
|
||||
tooltip: true,
|
||||
render: (h: any, p: any) => h("span", {}, (p.row.classes || []).map((c: any) => c.className).join("、") || "—"),
|
||||
},
|
||||
{ title: "更新时间", width: 140, align: "left", render: (h: any, p: any) => h("span", {}, fmtTime(p.row.updateTime)) },
|
||||
{ title: "截止时间", width: 140, align: "left", render: (h: any, p: any) => h("span", {}, fmtTime(p.row.deadlineAt)) },
|
||||
{
|
||||
title: "状态",
|
||||
width: 110,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => {
|
||||
const row = p.row as HomeworkV2Vo;
|
||||
const st = row.hwStatus === "DRAFT_SCHEDULED" ? `定时 ${fmtTime(row.scheduledAt)}` : "未发布";
|
||||
return h("Tag", { color: STATUS_COLOR[row.hwStatus] || "default" }, () => st);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 250,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
render: (h: any, p: any) => {
|
||||
const row = p.row as HomeworkV2Vo;
|
||||
const nodes: any[] = [];
|
||||
nodes.push(btn("编辑", () => edit(row)));
|
||||
nodes.push(btn("发布", () => publishDraft(row)));
|
||||
if (row.hwStatus === "DRAFT_SCHEDULED") nodes.push(btn("取消定时", () => cancelSchedule(row)));
|
||||
nodes.push(btn("删除", () => del(row)));
|
||||
return nodes;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const columns = computed(() => (viewMode.value === "drafts" ? draftColumns.value : pubColumns.value));
|
||||
const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 1. 操作 + 视图切换 -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="router.push('/list/new')">布置作业</Button>
|
||||
</div>
|
||||
<Tabs :value="viewMode" @on-click="switchView" class="mb-12">
|
||||
<TabPane name="published" label="已发布"></TabPane>
|
||||
<TabPane name="drafts" label="草稿箱"></TabPane>
|
||||
</Tabs>
|
||||
|
||||
<!-- 2. 已发布状态筛选 + 搜索区 -->
|
||||
<div class="search-wrap fix">
|
||||
<div v-if="viewMode === 'published'" class="fl">
|
||||
<Tabs :value="pubStatus" @on-click="onPubTab" size="small" class="inner-tabs">
|
||||
<TabPane v-for="t in PUB_TABS" :key="t.name" :name="t.name" :label="t.label"></TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Select v-model="subject" class="search-wrap_sel ml-12 fl" clearable placeholder="学科" style="width: 120px" @on-change="doSearch">
|
||||
<Option v-for="s in SUBJECTS" :key="s" :value="s">{{ s }}</Option>
|
||||
</Select>
|
||||
<Input
|
||||
class="fr"
|
||||
style="width: 200px"
|
||||
search
|
||||
suffix="ios-search"
|
||||
placeholder="输入关键字搜索"
|
||||
v-model="keyword"
|
||||
@on-search="doSearch"
|
||||
@on-clear="doSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 3. 表格 + 分页 -->
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="page.content"
|
||||
class="link-table table-bottom-box"
|
||||
:height="tableHeight"
|
||||
:loading="loading"
|
||||
no-data-text="暂无数据"
|
||||
></Table>
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle"></div>
|
||||
<Page
|
||||
class="page-box fr"
|
||||
:total="page.total"
|
||||
:current="pageNum"
|
||||
:page-size="pageSize"
|
||||
:page-size-opts="[10, 20, 30, 40]"
|
||||
@on-page-size-change="pageSizeUpdate"
|
||||
@on-change="pageUpdate"
|
||||
placement="top"
|
||||
show-total
|
||||
show-sizer
|
||||
></Page>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 延长截止 -->
|
||||
<Modal v-model="extendModal.open" title="延长截止时间" width="420px">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="新截止时间:">
|
||||
<DatePicker
|
||||
type="datetime"
|
||||
transfer
|
||||
v-model="extendModal.time"
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
placeholder="选择新的截止时间"
|
||||
style="width: 240px"
|
||||
></DatePicker>
|
||||
<div class="extend-tip" v-if="extendModal.row?.deadlineAt">
|
||||
原截止时间:{{ extendModal.row.deadlineAt }}
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="extendModal.saving" @click="doExtend">保存</Button>
|
||||
<Button @click="extendModal.open = false" style="margin-left: 8px">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-wrap {
|
||||
.inner-tabs {
|
||||
margin-bottom: -8px;
|
||||
:deep(.ivu-tabs-bar) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.extend-tip {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user