feat: 提交课程作业前端工程源码(Vue3 + Vite + view-ui-plus)
This commit is contained in:
@@ -0,0 +1,299 @@
|
|||||||
|
import { Method } from "alova";
|
||||||
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
|
import conf from "@/assets/config/host.config";
|
||||||
|
import type {
|
||||||
|
HomeworkVo,
|
||||||
|
HomeworkDetailVo,
|
||||||
|
HomeworkPageResult,
|
||||||
|
BasePageResult,
|
||||||
|
UpFileResultVo,
|
||||||
|
UpFileVo,
|
||||||
|
InitializeRedVo,
|
||||||
|
NextPrevVo,
|
||||||
|
ReadGroup,
|
||||||
|
HomeworkStatisticsVo,
|
||||||
|
HomeworkRequest,
|
||||||
|
HomeworkQuery,
|
||||||
|
DictOption,
|
||||||
|
DeptManageItem,
|
||||||
|
ContactGroupPage,
|
||||||
|
ContactUserPage,
|
||||||
|
SchoolTreeNode,
|
||||||
|
ClassTreeNode,
|
||||||
|
SubmissionsResultVo,
|
||||||
|
SubmissionDetailVo,
|
||||||
|
GradeRequest,
|
||||||
|
MyHomeworksVo,
|
||||||
|
MySubmissionVo,
|
||||||
|
SubmitRequest,
|
||||||
|
} from "@/types/homework";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。
|
||||||
|
*
|
||||||
|
* 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名;
|
||||||
|
* platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。
|
||||||
|
* 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。
|
||||||
|
*
|
||||||
|
* 变更要点(P0):
|
||||||
|
* 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List)
|
||||||
|
* 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down)
|
||||||
|
* 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete)
|
||||||
|
* 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)
|
||||||
|
* 5. 参数更名:chaoSend→fileIds、topmost→smsFlag
|
||||||
|
* 6. Delete 入参从 ids 逗号串改为 List<Integer> JSON 数组
|
||||||
|
*
|
||||||
|
* 变更要点(P1-7):
|
||||||
|
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
|
||||||
|
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HOMEWORK = "/api/homework";
|
||||||
|
const BASE = "/api/base";
|
||||||
|
const DICT = "/api/dict";
|
||||||
|
const FILE = "/api/file";
|
||||||
|
const CONTACTS = "/api/contacts";
|
||||||
|
|
||||||
|
/** GET 请求,T 为返回数据类型 */
|
||||||
|
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||||
|
return new Method<T>("GET", alovaInstance, url, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */
|
||||||
|
function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Method<T> {
|
||||||
|
return new Method<T>(
|
||||||
|
"POST",
|
||||||
|
alovaInstance,
|
||||||
|
url,
|
||||||
|
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
|
||||||
|
data ? JSON.stringify(data) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */
|
||||||
|
function postForm<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||||
|
const pairs = data
|
||||||
|
? Object.entries(data).filter(([, v]) => v !== undefined && v !== null)
|
||||||
|
: [];
|
||||||
|
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
||||||
|
// 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收,
|
||||||
|
// 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
|
||||||
|
const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||||
|
return new Method<T>(
|
||||||
|
"POST",
|
||||||
|
alovaInstance,
|
||||||
|
fullUrl,
|
||||||
|
{ headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } },
|
||||||
|
form || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件下载直链(浏览器 location.href 跳转) */
|
||||||
|
function fileDownUrl(fileId: number): string {
|
||||||
|
return `${conf.serviceHost}${FILE}/Down?id=${fileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业全部附件打包下载直链 */
|
||||||
|
function allFileDownUrl(dataid: number): string {
|
||||||
|
return `${conf.serviceHost}${HOMEWORK}/AllFileDown?dataid=${dataid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseApi {
|
||||||
|
// ========== 用户信息 & 模块权限 ==========
|
||||||
|
/** 获取当前登录用户信息 */
|
||||||
|
getUserInfo() {
|
||||||
|
return get<SchoolTreeNode>(`${BASE}/GetUserInfo`);
|
||||||
|
}
|
||||||
|
/** 获取当前用户已授权的模块列表 */
|
||||||
|
getUserMod() {
|
||||||
|
return get<SchoolTreeNode[]>(`${BASE}/GetUserMod`);
|
||||||
|
}
|
||||||
|
/** 当前学校组织树(部门→教师,用于选择用户) */
|
||||||
|
getSchoolAll() {
|
||||||
|
return get<SchoolTreeNode[]>(`${BASE}/GetSchoolAll`);
|
||||||
|
}
|
||||||
|
/** 年级→班级树(学生场景:选择通知对象到班级一级) */
|
||||||
|
getClassTree() {
|
||||||
|
return get<ClassTreeNode[]>(`${BASE}/GetClassTree`);
|
||||||
|
}
|
||||||
|
/** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */
|
||||||
|
setRole(as: "teacher" | "student" | "parent") {
|
||||||
|
return postJSON<{ role: string }>(`${BASE}/SetRole`, { as });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeworkApi {
|
||||||
|
// ========== 收件箱 ==========
|
||||||
|
/** 已收作业列表 */
|
||||||
|
list(params: HomeworkQuery) {
|
||||||
|
return get<HomeworkPageResult<HomeworkVo>>(`${HOMEWORK}/List`, params);
|
||||||
|
}
|
||||||
|
/** 清单:一键全部已读 */
|
||||||
|
initializeRed() {
|
||||||
|
return postForm<InitializeRedVo>(`${HOMEWORK}/InitializeRed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 已发/全部作业 ==========
|
||||||
|
/** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */
|
||||||
|
sendList(params: HomeworkQuery) {
|
||||||
|
return get<BasePageResult<HomeworkVo>>(`${HOMEWORK}/SendList`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 详情 ==========
|
||||||
|
/** 查看详情(type=1 标读;type=0 编辑不标读) */
|
||||||
|
modify(serid: number, type = 1) {
|
||||||
|
return get<HomeworkDetailVo>(`${HOMEWORK}/Detail`, { serid, type });
|
||||||
|
}
|
||||||
|
/** 编辑回显(不标读) */
|
||||||
|
getHomeworkDetails(serid: number) {
|
||||||
|
return get<HomeworkDetailVo>(`${HOMEWORK}/GetDetail`, { serid });
|
||||||
|
}
|
||||||
|
/** 上一篇/下一篇 */
|
||||||
|
getNextPrevHomework(serid: number, type: string) {
|
||||||
|
return get<NextPrevVo>(`${HOMEWORK}/GetNextPrev`, { serid, type });
|
||||||
|
}
|
||||||
|
/** 阅读情况(已读/未读分组) */
|
||||||
|
orgizeModify(serid: number) {
|
||||||
|
return get<ReadGroup[]>(`${HOMEWORK}/ReadDetail`, { serid });
|
||||||
|
}
|
||||||
|
/** 追加接收人 */
|
||||||
|
receiveUsers(serid: number, users: string) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/ReceiveUsers`, { serid, users });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 发布/编辑/删除 ==========
|
||||||
|
/** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */
|
||||||
|
create(data: HomeworkRequest) {
|
||||||
|
return postJSON<number>(`${HOMEWORK}/Create`, data);
|
||||||
|
}
|
||||||
|
/** 编辑作业(JSON body,含 id) */
|
||||||
|
editHomework(data: HomeworkRequest) {
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Edit`, data);
|
||||||
|
}
|
||||||
|
/** 删除作业(ids 数组,支持批量) */
|
||||||
|
deleteHomework(ids: number | string) {
|
||||||
|
// 兼容页面传单个 id 或逗号串,统一转成数组
|
||||||
|
const idArr = String(ids).split(",").map(Number).filter(Boolean);
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Delete`, idArr);
|
||||||
|
}
|
||||||
|
/** 撤回 */
|
||||||
|
userPull(serid: number) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/UserPull`, { serid });
|
||||||
|
}
|
||||||
|
/** 置顶/取消置顶 */
|
||||||
|
setTopmost(serid: number, topmost = 1) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/SetTopmost`, { serid, topmost });
|
||||||
|
}
|
||||||
|
/** 催收未读 */
|
||||||
|
expedite(serid: number) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/Expedite`, { serid });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 课程作业 v1:提交情况 / 批改 / 催交 ==========
|
||||||
|
/** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */
|
||||||
|
submissions(serid: number, submitStatus = "all") {
|
||||||
|
return get<SubmissionsResultVo>(`${HOMEWORK}/Submissions`, { serid, submitStatus });
|
||||||
|
}
|
||||||
|
/** 单份提交详情(批改工作台右侧) */
|
||||||
|
submissionDetail(subId: number) {
|
||||||
|
return get<SubmissionDetailVo>(`${HOMEWORK}/SubmissionDetail`, { subId });
|
||||||
|
}
|
||||||
|
/** 批改(JSON body):action=grade/redo/excellent */
|
||||||
|
grade(data: GradeRequest) {
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Grade`, data);
|
||||||
|
}
|
||||||
|
/** 催交未交学生 */
|
||||||
|
remind(serid: number) {
|
||||||
|
return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== v2 学生端 ==========
|
||||||
|
/** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */
|
||||||
|
myHomeworks() {
|
||||||
|
return get<MyHomeworksVo>(`${HOMEWORK}/MyHomeworks`);
|
||||||
|
}
|
||||||
|
/** 我对某作业的详情(作业信息 + 我的提交/批改) */
|
||||||
|
mySubmission(serid: number) {
|
||||||
|
return get<MySubmissionVo>(`${HOMEWORK}/MySubmission`, { serid });
|
||||||
|
}
|
||||||
|
/** 提交 / 订正重交(JSON body:serid, text, images, attachments) */
|
||||||
|
submit(data: SubmitRequest) {
|
||||||
|
return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 附件 ==========
|
||||||
|
fileDownUrl = fileDownUrl;
|
||||||
|
allFileDownUrl = allFileDownUrl;
|
||||||
|
|
||||||
|
// ========== 部门字典 ==========
|
||||||
|
/** 发文部门下拉 */
|
||||||
|
getSysDic() {
|
||||||
|
return get<DictOption[]>(`${DICT}/List`, { type: 1 });
|
||||||
|
}
|
||||||
|
/** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */
|
||||||
|
getSchModuleIsSms() {
|
||||||
|
return get<boolean>(`${DICT}/Sync`);
|
||||||
|
}
|
||||||
|
/** 作业统计 */
|
||||||
|
statistics(params?: { starttime?: string; endtime?: string }) {
|
||||||
|
return get<HomeworkStatisticsVo>(`${HOMEWORK}/Statis`, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingApi {
|
||||||
|
// ========== 发文部门管理 ==========
|
||||||
|
/** 获取发文部门管理列表 */
|
||||||
|
getDictMangeList() {
|
||||||
|
return get<DeptManageItem[]>(`${DICT}/ManageList`, { type: "dept" });
|
||||||
|
}
|
||||||
|
/** 新增发文部门 */
|
||||||
|
addDeptDic(name: string) {
|
||||||
|
return postForm<void>(`${DICT}/Add`, { name });
|
||||||
|
}
|
||||||
|
/** 更新发文部门(名称、启用状态) */
|
||||||
|
updateDeptDic(SerID: number, Name: string, Enable = 1) {
|
||||||
|
return postForm<void>(`${DICT}/Update`, { SerID, Name, Enable });
|
||||||
|
}
|
||||||
|
/** 删除发文部门(原 GET DelDept,已 POST 化) */
|
||||||
|
delDept(serid: number) {
|
||||||
|
return postForm<void>(`${DICT}/Delete`, { serid });
|
||||||
|
}
|
||||||
|
/** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */
|
||||||
|
sortDeptDic(ids: string) {
|
||||||
|
return postForm<void>(`${DICT}/Sort`, { ids });
|
||||||
|
}
|
||||||
|
// ========== 自定义组管理 ==========
|
||||||
|
/** 获取联系组列表 */
|
||||||
|
getGroup(page: number, count: number) {
|
||||||
|
return get<ContactGroupPage>(`${CONTACTS}/GetGroup`, { page, count });
|
||||||
|
}
|
||||||
|
/** 新增联系组 */
|
||||||
|
addGroup(name: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/AddGroup`, { name });
|
||||||
|
}
|
||||||
|
/** 编辑联系组 */
|
||||||
|
editGroup(id: number, name: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/EditGroup`, { name, id });
|
||||||
|
}
|
||||||
|
/** 删除联系组 */
|
||||||
|
delGroup(ids: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/DelGroup`, { ids });
|
||||||
|
}
|
||||||
|
/** 获取联系组用户 */
|
||||||
|
getContactsTea(gid: number, page = 0, count = 0) {
|
||||||
|
return get<ContactUserPage>(`${CONTACTS}/GetContactsTea`, { gid, page, count });
|
||||||
|
}
|
||||||
|
/** 设置联系组用户 */
|
||||||
|
setContactsTea(gid: number, ids: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/SetContactsTea`, { gid, ids });
|
||||||
|
}
|
||||||
|
/** 联系组用户树(选择用户-自定义组 tab) */
|
||||||
|
getGroupTree() {
|
||||||
|
return get<SchoolTreeNode[]>(`${CONTACTS}/GetGroupTree`, { type: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseApi = new BaseApi();
|
||||||
|
export const homeworkApi = new HomeworkApi();
|
||||||
|
export const settingApi = new SettingApi();
|
||||||
|
export default homeworkApi;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { IModule } from "@/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。
|
||||||
|
* 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。
|
||||||
|
* 注:课程作业不做审批,故不含「审批列表/审核设置」;平台模块配置若仍下发 modId 5/6,
|
||||||
|
* 由 utils/menuRoute 的 DROPPED_MOD_IDS 统一剔除(见 classwork-frontend-m5-plan.md §6)。
|
||||||
|
*/
|
||||||
|
export function getMenuList(): Promise<IModule[]> {
|
||||||
|
const menu: IModule[] = [
|
||||||
|
{ id: 1, modId: 1, name: "已收到", page: "/" },
|
||||||
|
{ id: 2, modId: 2, name: "已发送", page: "/list" },
|
||||||
|
{ id: 3, modId: 3, name: "全部作业", page: "/homeworklist" },
|
||||||
|
{ id: 4, modId: 4, name: "作业统计", page: "/stats" },
|
||||||
|
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
|
||||||
|
];
|
||||||
|
return Promise.resolve(menu);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="type_set-box">
|
||||||
|
<div class="type_name">
|
||||||
|
<i class="iconfont icon-stop fl mr-10" v-if="!isEdit && !active" style="color: #ff5959"></i>
|
||||||
|
<Input v-if="isEdit" v-model="type.name" ref="type_input" @on-blur="update" @on-enter="update" />
|
||||||
|
<span v-else class="fl">{{ type.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="type_action flex">
|
||||||
|
<div class="action_item" @click="open">
|
||||||
|
<span v-if="active">停用</span>
|
||||||
|
<span v-else>启用</span>
|
||||||
|
</div>
|
||||||
|
<div class="action_edit" @click="edit">
|
||||||
|
<span>编辑</span>
|
||||||
|
</div>
|
||||||
|
<div class="action_delete" @click="remove">
|
||||||
|
<span>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="divider2"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "TypeSetBox",
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
name: { type: String, default: "" },
|
||||||
|
id: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
active: { type: Boolean, default: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEdit: false,
|
||||||
|
eventType: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
type: {
|
||||||
|
deep: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (this.isEdit) {
|
||||||
|
this.isEdit = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type.name"(newVal) {
|
||||||
|
if (this.isEdit) {
|
||||||
|
this.isEdit = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.$emit("active", this.type.id);
|
||||||
|
},
|
||||||
|
edit() {
|
||||||
|
this.isEdit = !this.isEdit;
|
||||||
|
if (this.isEdit) this.$nextTick(() => this.$refs["type_input"].focus());
|
||||||
|
},
|
||||||
|
update(e) {
|
||||||
|
if (!this.type.name) {
|
||||||
|
this.$Message.error("值不得为空!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.eventType == "") {
|
||||||
|
this.eventType = e.type;
|
||||||
|
this.isEdit = false;
|
||||||
|
this.$emit("edit", this.type.id, this.type.name);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.eventType = "";
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove() {
|
||||||
|
this.$emit("remove", this.type.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.type_set-box {
|
||||||
|
width: 222px;
|
||||||
|
background: #ffffff;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
.type_name {
|
||||||
|
height: 66px;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
border-bottom: none;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
line-height: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
span {
|
||||||
|
height: 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: PingFangSC-Semibold, PingFang SC;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #272829;
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 222px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.type_action {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 222px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-radius: 0px 0px 2px 2px;
|
||||||
|
border-top: none;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
div:hover {
|
||||||
|
color: #6c91f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.divider {
|
||||||
|
// position: relative;
|
||||||
|
height: 14px;
|
||||||
|
width: 1px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
// left: 73px;
|
||||||
|
// top: -23px;
|
||||||
|
float: left;
|
||||||
|
margin-left: 71px;
|
||||||
|
margin-top: -22px;
|
||||||
|
}
|
||||||
|
.divider2 {
|
||||||
|
// position: relative;
|
||||||
|
// left: 146px;
|
||||||
|
// top: -37px;
|
||||||
|
height: 14px;
|
||||||
|
width: 1px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
float: left;
|
||||||
|
margin-left: 147px;
|
||||||
|
margin-top: -22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 作业对象选择:全部用户 / 指定用户(自旧前端 user_check_input.vue 适配) -->
|
||||||
|
<div class="check-user-wrap">
|
||||||
|
<div class="check-user-box">
|
||||||
|
<RadioGroup v-model="isCheckUserAll" class="radio" @on-change="userAllCheck">
|
||||||
|
<Radio :disabled="editDisabled" :label="0">{{ isEdit ? "现有" : "全部" }}用户</Radio>
|
||||||
|
<Radio :label="1">指定用户</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
v-model="userNameStr"
|
||||||
|
readonly
|
||||||
|
:disabled="!isCheckUserAll"
|
||||||
|
placeholder="选择指定的用户"
|
||||||
|
class="input"
|
||||||
|
></Input>
|
||||||
|
<Button icon="md-add" @click="checkUserShow" :disabled="!isCheckUserAll"></Button>
|
||||||
|
<Button icon="md-trash" @click="resetUser" :disabled="!isCheckUserAll"></Button>
|
||||||
|
</div>
|
||||||
|
<UserCheckModal
|
||||||
|
ref="userCheckModal"
|
||||||
|
:sources="sources"
|
||||||
|
:userids="tempUserIds"
|
||||||
|
@on-submit="submitUser"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
/*
|
||||||
|
* 作业对象选择输入(自旧前端 oldfront/src/components/user_check_input.vue 适配 Vue3)。
|
||||||
|
* v-model 值为 "all" 或用户 id 逗号串;切换到「指定用户」直接弹出选择用户弹窗。
|
||||||
|
*/
|
||||||
|
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||||
|
import { baseApi, settingApi } from "@/api/module/homework";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UserCheckInput",
|
||||||
|
components: { UserCheckModal },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "all",
|
||||||
|
},
|
||||||
|
/** 编辑态:全部用户选项禁用为「现有用户」 */
|
||||||
|
isEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isCheckUserAll: 0 as number,
|
||||||
|
tempUserIds: [] as any[],
|
||||||
|
userNameStr: "",
|
||||||
|
editDisabled: false,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
name: "基础平台",
|
||||||
|
loader: () => baseApi.getSchoolAll().send() as Promise<any[]>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "自定义组",
|
||||||
|
loader: () => settingApi.getGroupTree().send() as Promise<any[]>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.modelValue === "all") {
|
||||||
|
this.isCheckUserAll = 0;
|
||||||
|
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
|
||||||
|
} else {
|
||||||
|
this.isCheckUserAll = 1;
|
||||||
|
this.tempUserIds = this.modelValue.split(",").map((x) => parseInt(x)).filter((x) => !isNaN(x));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkUserShow() {
|
||||||
|
(this.$refs.userCheckModal as any).show();
|
||||||
|
},
|
||||||
|
resetUser() {
|
||||||
|
//清除用户
|
||||||
|
this.tempUserIds = [];
|
||||||
|
this.userNameStr = "";
|
||||||
|
this.$emit("update:modelValue", "");
|
||||||
|
},
|
||||||
|
userAllCheck(type: number) {
|
||||||
|
let tempIds: any = String(this.modelValue || "")
|
||||||
|
.split(",")
|
||||||
|
.map((x) => parseInt(x))
|
||||||
|
.filter((x) => !isNaN(x));
|
||||||
|
if (type == 0) {
|
||||||
|
if (this.isEdit) {
|
||||||
|
this.tempUserIds = tempIds;
|
||||||
|
this.userNameStr = "现有用户";
|
||||||
|
this.$emit("update:modelValue", String(tempIds));
|
||||||
|
} else {
|
||||||
|
this.tempUserIds = [];
|
||||||
|
this.userNameStr = "全部用户";
|
||||||
|
this.$emit("update:modelValue", "all");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.isEdit) {
|
||||||
|
this.editDisabled = true;
|
||||||
|
}
|
||||||
|
this.tempUserIds = tempIds;
|
||||||
|
this.userNameStr = "";
|
||||||
|
this.checkUserShow();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitUser(arr: any[]) {
|
||||||
|
//确认用户
|
||||||
|
this.userNameStr = arr.map((x) => x.name).join("、");
|
||||||
|
const ids = arr.map((x) => x.id).join(",");
|
||||||
|
this.$emit("update:modelValue", ids || "all");
|
||||||
|
if (!arr.length) {
|
||||||
|
// 清空后回到全部用户
|
||||||
|
this.isCheckUserAll = 0;
|
||||||
|
this.userNameStr = "全部用户";
|
||||||
|
this.$emit("update:modelValue", "all");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue(val: string) {
|
||||||
|
if (val === "all") {
|
||||||
|
this.isCheckUserAll = 0;
|
||||||
|
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
|
||||||
|
} else if (val === "") {
|
||||||
|
this.userNameStr = "";
|
||||||
|
} else {
|
||||||
|
// 编辑回显:指定用户 id 串
|
||||||
|
this.isCheckUserAll = 1;
|
||||||
|
this.tempUserIds = val
|
||||||
|
.split(",")
|
||||||
|
.map((x) => parseInt(x))
|
||||||
|
.filter((x) => !isNaN(x));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.check-user-wrap {
|
||||||
|
width: 100%;
|
||||||
|
.check-user-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
.radio {
|
||||||
|
width: 180px;
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程作业模块路由(扁平短路由,与基础平台模块 path 一致)。
|
||||||
|
* 页面间参数通过 query 传递:
|
||||||
|
* - /list/new?serid= (编辑)
|
||||||
|
* - /list/new?fid= (转发)
|
||||||
|
* - /list/new?tempid=(模板引用)
|
||||||
|
* - /list/detail?serid=&type=、/homeworklist/detail?serid=&type=
|
||||||
|
* type: 0=编辑视角(不标读) 1=普通查看(标读)
|
||||||
|
*
|
||||||
|
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据):
|
||||||
|
* 1=已收到 2=已发送 3=全部作业 4=作业统计 21=应用设置
|
||||||
|
* 发作业为「已发送」的深层子页(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 },
|
||||||
|
},
|
||||||
|
// 我布置的(教师端作业列表)
|
||||||
|
{
|
||||||
|
path: "/list",
|
||||||
|
name: "homework_list",
|
||||||
|
component: () => import("@/views/homework/HomeworkList.vue"),
|
||||||
|
meta: { title: "我布置的", modId: 2 },
|
||||||
|
},
|
||||||
|
// 提交情况(某作业的已交/未交名单;从列表行或菜单进入,无 serid 时页内选择作业)
|
||||||
|
{
|
||||||
|
path: "/submissions",
|
||||||
|
name: "homework_submissions",
|
||||||
|
component: () => import("@/views/homework/Submissions.vue"),
|
||||||
|
meta: { title: "提交情况", modId: 25 },
|
||||||
|
},
|
||||||
|
// 批改中心(待批作业→逐份批改工作台;无 serid 时先看待批作业列表)
|
||||||
|
{
|
||||||
|
path: "/grade",
|
||||||
|
name: "homework_grade",
|
||||||
|
component: () => import("@/views/homework/Grade.vue"),
|
||||||
|
meta: { title: "批改中心", modId: 26 },
|
||||||
|
},
|
||||||
|
// 发作业(新建/编辑/转发/模板):已发送的深层子页,不在菜单里,
|
||||||
|
// layout.vue 按.meta.parentId 推导面包屑「已发送>发作业」并高亮父菜单
|
||||||
|
{
|
||||||
|
path: "/list/new",
|
||||||
|
name: "new_homework",
|
||||||
|
component: () => import("@/views/homework/NewHomework.vue"),
|
||||||
|
meta: { title: "发作业", modId: 22, parentId: 2 },
|
||||||
|
},
|
||||||
|
// 全部作业(管理端)
|
||||||
|
{
|
||||||
|
path: "/homeworklist",
|
||||||
|
name: "homework_manage",
|
||||||
|
component: () => import("@/views/homework/HomeworkList.vue"),
|
||||||
|
meta: { title: "全部作业", modId: 3 },
|
||||||
|
},
|
||||||
|
// 作业详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
|
||||||
|
// 已发送下 /list/detail,全部作业下 /homeworklist/detail;type 区分是否标读)
|
||||||
|
{
|
||||||
|
path: "/list/detail",
|
||||||
|
name: "homework_list_detail",
|
||||||
|
component: () => import("@/views/homework/HomeworkDetails.vue"),
|
||||||
|
meta: { title: "作业详情", modId: 23, parentId: 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/homeworklist/detail",
|
||||||
|
name: "homework_manage_detail",
|
||||||
|
component: () => import("@/views/homework/HomeworkDetails.vue"),
|
||||||
|
meta: { title: "作业详情", modId: 24, parentId: 3 },
|
||||||
|
},
|
||||||
|
// 作业统计(旧版行为:整页跳转外部统计系统)
|
||||||
|
{
|
||||||
|
path: "/stats",
|
||||||
|
name: "homework_stats",
|
||||||
|
component: () => import("@/views/homework/HomeworkStats.vue"),
|
||||||
|
meta: { title: "作业统计", modId: 4 },
|
||||||
|
},
|
||||||
|
// 应用设置(发文部门 + 自定义组)
|
||||||
|
{
|
||||||
|
path: "/setting",
|
||||||
|
name: "homework_setting",
|
||||||
|
component: () => import("@/views/homework/HomeworkSetting.vue"),
|
||||||
|
meta: { title: "应用设置", modId: 21 },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---- v2 学生端 ---- */
|
||||||
|
// 学生·今日作业
|
||||||
|
{
|
||||||
|
path: "/stu/today",
|
||||||
|
name: "student_today",
|
||||||
|
component: () => import("@/views/homework/StudentHomeworks.vue"),
|
||||||
|
meta: { title: "今日作业", modId: 31, role: "student" },
|
||||||
|
},
|
||||||
|
// 学生·待订正(复用同一页,默认过滤待订正)
|
||||||
|
{
|
||||||
|
path: "/stu/redo",
|
||||||
|
name: "student_redo",
|
||||||
|
component: () => import("@/views/homework/StudentHomeworks.vue"),
|
||||||
|
meta: { title: "待订正", modId: 32, role: "student", initTab: "redo" },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
||||||
|
{ path: "/details", redirect: "/list/detail" },
|
||||||
|
{ path: "/newhomework", redirect: "/list/new" },
|
||||||
|
{ path: "/edit", redirect: "/list/new" },
|
||||||
|
{ path: "/manage", redirect: "/homeworklist" },
|
||||||
|
{ path: "/homework", redirect: "/accept" },
|
||||||
|
{ path: "/homework/newhomework", redirect: "/list/new" },
|
||||||
|
{ path: "/homework/edit", redirect: "/list/new" },
|
||||||
|
{ path: "/homework/homework_accept", redirect: "/accept" },
|
||||||
|
{ path: "/homework/homework_list", redirect: "/list" },
|
||||||
|
{ path: "/homework/manage", redirect: "/homeworklist" },
|
||||||
|
{ path: "/homework/homework_details", redirect: "/list/detail" },
|
||||||
|
{ path: "/homework/statistics", redirect: "/stats" },
|
||||||
|
{ path: "/homework/setting", redirect: "/setting" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
/**
|
||||||
|
* 课程作业前端类型契约(P1-7)
|
||||||
|
*
|
||||||
|
* 与后端 VO/DTO 一一对齐:
|
||||||
|
* - HomeworkVo → 后端 model/homework/HomeworkVo.java(Jackson 默认驼峰序列化)
|
||||||
|
* - HomeworkDetailVo → 后端 model/homework/HomeworkDetailVo.java(@JsonProperty 保留旧字段名)
|
||||||
|
* - BasePageResult → 后端 model/BasePageResult.java
|
||||||
|
* - HomeworkPageResult extends BasePageResult → 后端 model/homework/HomeworkPageResult.java
|
||||||
|
* - UpFileResultVo → 后端 model/homework/UpFileResultVo.java(@JsonProperty)
|
||||||
|
* - UpFileVo → 后端 model/homework/UpFileVo.java
|
||||||
|
* - InitializeRedVo → 后端 model/homework/InitializeRedVo.java(@JsonProperty)
|
||||||
|
* - HomeworkStatisticsVo → 后端 model/homework/HomeworkStatisticsVo.java
|
||||||
|
* - HomeworkRequest → 后端 model/homework/HomeworkRequest.java
|
||||||
|
* - HomeworkQuery → 后端 model/homework/HomeworkQuery.java + BasePageQuery.java
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================ 分页外壳 ============================
|
||||||
|
|
||||||
|
/** 统一分页响应外壳(对齐 BasePageResult.java) */
|
||||||
|
export interface BasePageResult<T> {
|
||||||
|
total: number;
|
||||||
|
content: T[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */
|
||||||
|
export interface HomeworkPageResult<T> extends BasePageResult<T> {
|
||||||
|
/** 当前用户未读作业总数 */
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 作业列表项 ============================
|
||||||
|
|
||||||
|
/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */
|
||||||
|
export interface HomeworkVo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type: number;
|
||||||
|
typeDesc: string;
|
||||||
|
publisherAccount: string;
|
||||||
|
publisherName: string;
|
||||||
|
deptId: string;
|
||||||
|
deptName: string;
|
||||||
|
status: number;
|
||||||
|
statusDesc: string;
|
||||||
|
topmost: number;
|
||||||
|
/** 已读人数 */
|
||||||
|
readCount: number;
|
||||||
|
/** 应读人数 */
|
||||||
|
allowCount: number;
|
||||||
|
/** 当前用户是否已读 */
|
||||||
|
read: boolean;
|
||||||
|
/** 是否撤回:0 否 / 1 是 */
|
||||||
|
isPulled: number;
|
||||||
|
/** 定时发送时间 */
|
||||||
|
timingSendTime: string | null;
|
||||||
|
/** 父作业ID(转发时关联原作业) */
|
||||||
|
parentId: number | null;
|
||||||
|
/** 短信发送标记 */
|
||||||
|
smsFlag: number;
|
||||||
|
/** 接收对象类型:class=班级(学生场景) */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 接收列表:"all" 或 班级 id 逗号串 */
|
||||||
|
receiveList: string | null;
|
||||||
|
/** ===== 课程作业 v1(教师核心闭环)===== */
|
||||||
|
/** 学科 */
|
||||||
|
subject?: string;
|
||||||
|
/** 截止时间 */
|
||||||
|
deadline?: string | null;
|
||||||
|
/** 提交方式:text/image/attach/none */
|
||||||
|
submitType?: string[];
|
||||||
|
/** 作业流程状态:draft 草稿 / collecting 收集中 / closed 已截止 / done 已完成 */
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
hwStatusDesc?: string;
|
||||||
|
/** 提交/批改进度(草稿或仅查阅为 null) */
|
||||||
|
progress?: HomeworkProgress | null;
|
||||||
|
createBy: string | null;
|
||||||
|
createTime: string | null;
|
||||||
|
updateBy: string | null;
|
||||||
|
updateTime: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 作业详情 ============================
|
||||||
|
|
||||||
|
/** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */
|
||||||
|
export interface HomeworkDetailVo {
|
||||||
|
serid: number;
|
||||||
|
title: string;
|
||||||
|
/** 作业内容(旧字段名 Contents) */
|
||||||
|
Contents: string;
|
||||||
|
/** 发布部门ID(旧字段名 DeptID) */
|
||||||
|
DeptID: string;
|
||||||
|
/** 发布部门名称(旧字段名 DeptName) */
|
||||||
|
DeptName: string;
|
||||||
|
/** 发布人姓名(旧字段名 TeaName) */
|
||||||
|
TeaName: string;
|
||||||
|
/** 发布时间(旧字段名 EditDate,已格式化为字符串) */
|
||||||
|
EditDate: string;
|
||||||
|
/** 应读人数 */
|
||||||
|
numcount: number;
|
||||||
|
/** 已读人数 */
|
||||||
|
readnumcount: number;
|
||||||
|
/** 接收人列表 */
|
||||||
|
ReceiveList: string | null;
|
||||||
|
/** 定时发送时间(旧字段名 DsfsTime,已格式化) */
|
||||||
|
DsfsTime: string | null;
|
||||||
|
/** 是否定时发送 */
|
||||||
|
isDsfs: boolean;
|
||||||
|
/** ===== 课程作业 v1 ===== */
|
||||||
|
subject?: string;
|
||||||
|
deadline?: string | null;
|
||||||
|
submitType?: string[];
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
hwStatusDesc?: string;
|
||||||
|
progress?: HomeworkProgress | null;
|
||||||
|
/** 附件列表(旧字段名 ChaoSend) */
|
||||||
|
ChaoSend: HomeworkDetailFileItem[];
|
||||||
|
/** 接收对象列表:"all" 或 班级 id 数组 */
|
||||||
|
tlist: string | string[];
|
||||||
|
/** 接收对象类型:class=班级 */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 接收对象可读文本(全部班级 / 班级名串) */
|
||||||
|
receiveText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */
|
||||||
|
export interface HomeworkDetailFileItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
fileid: number;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 文件上传 ============================
|
||||||
|
|
||||||
|
/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */
|
||||||
|
export interface UpFileResultVo {
|
||||||
|
fileid: number;
|
||||||
|
name: string;
|
||||||
|
fileurl: string;
|
||||||
|
host: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件视图对象(对齐 UpFileVo.java) */
|
||||||
|
export interface UpFileVo {
|
||||||
|
id: number;
|
||||||
|
fileTrueName: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: number;
|
||||||
|
fileType: string;
|
||||||
|
topicId: number;
|
||||||
|
topicType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 一键已读 ============================
|
||||||
|
|
||||||
|
/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */
|
||||||
|
export interface InitializeRedVo {
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 上一篇/下一篇 ============================
|
||||||
|
|
||||||
|
/** 上一篇/下一篇作业ID */
|
||||||
|
export interface NextPrevVo {
|
||||||
|
prevId: number;
|
||||||
|
nextId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 阅读情况 ============================
|
||||||
|
|
||||||
|
/** 阅读情况分组项(后端 Map 返回,字段名固定) */
|
||||||
|
export interface ReadGroup {
|
||||||
|
GroupName: string;
|
||||||
|
teaslist: ReadGroupUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 阅读情况用户项(后端 Map 返回) */
|
||||||
|
export interface ReadGroupUser {
|
||||||
|
account: string;
|
||||||
|
teaname: string;
|
||||||
|
states: 'read' | 'unread';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 统计 ============================
|
||||||
|
|
||||||
|
/** 作业统计视图(对齐 HomeworkStatisticsVo.java) */
|
||||||
|
export interface HomeworkStatisticsVo {
|
||||||
|
total: number;
|
||||||
|
publishedCount: number;
|
||||||
|
totalReadRecords: number;
|
||||||
|
unreadForCurrentUser: number;
|
||||||
|
byPublisher: { publisherName: string; count: number }[];
|
||||||
|
byMonth: { year: number; month: number; count: number }[];
|
||||||
|
// ===== 课程作业口径(v1,可选) =====
|
||||||
|
/** 我布置的作业数 */
|
||||||
|
myAssignCount?: number;
|
||||||
|
/** 待批改总量 */
|
||||||
|
pendingGrade?: number;
|
||||||
|
/** 平均提交率(%) */
|
||||||
|
avgSubmitRate?: number;
|
||||||
|
/** 按作业进度 */
|
||||||
|
byHomework?: { id: number; title: string; subject: string; hwStatus: HomeworkStatus } & HomeworkProgress[];
|
||||||
|
/** 分数分布 */
|
||||||
|
scoreDist?: { label: string; key: string; count: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 课程作业 v1:提交 / 批改 ============================
|
||||||
|
|
||||||
|
/** 作业流程状态 */
|
||||||
|
export type HomeworkStatus = "draft" | "collecting" | "closed" | "done";
|
||||||
|
|
||||||
|
/** 单个提交状态 */
|
||||||
|
export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone";
|
||||||
|
|
||||||
|
/** 提交/批改进度 */
|
||||||
|
export interface HomeworkProgress {
|
||||||
|
/** 应交人数 */
|
||||||
|
should: number;
|
||||||
|
/** 已交人数 */
|
||||||
|
submitted: number;
|
||||||
|
/** 已批改人数 */
|
||||||
|
graded: number;
|
||||||
|
/** 待批改人数 */
|
||||||
|
pendingGrade: number;
|
||||||
|
/** 待订正人数 */
|
||||||
|
needRedo: number;
|
||||||
|
/** 已订正人数 */
|
||||||
|
redone: number;
|
||||||
|
/** 未交人数 */
|
||||||
|
unsubmitted: number;
|
||||||
|
/** 提交率(%) */
|
||||||
|
submitRate: number;
|
||||||
|
/** 批改率(%) */
|
||||||
|
gradeRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交名单项(Submissions 接口返回,精简) */
|
||||||
|
export interface SubmissionBrief {
|
||||||
|
subId: number;
|
||||||
|
studentId: number;
|
||||||
|
studentName: string;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
hasGrade: boolean;
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
isExcellent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交名单按班级分组 */
|
||||||
|
export interface SubmissionGroup {
|
||||||
|
className: string;
|
||||||
|
students: SubmissionBrief[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 某作业提交情况结果 */
|
||||||
|
export interface SubmissionsResultVo {
|
||||||
|
homeworkId: number;
|
||||||
|
title: string;
|
||||||
|
subject: string;
|
||||||
|
deadline: string | null;
|
||||||
|
hwStatus: HomeworkStatus;
|
||||||
|
hwStatusDesc: string;
|
||||||
|
summary: HomeworkProgress;
|
||||||
|
groups: SubmissionGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批改结果 */
|
||||||
|
export interface GradeResult {
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
comment: string;
|
||||||
|
isExcellent: boolean;
|
||||||
|
markedImages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单份提交详情(批改工作台用) */
|
||||||
|
export interface SubmissionDetailVo {
|
||||||
|
subId: number;
|
||||||
|
homeworkId: number;
|
||||||
|
homeworkTitle: string;
|
||||||
|
subject: string;
|
||||||
|
studentId: number;
|
||||||
|
studentName: string;
|
||||||
|
classId: string;
|
||||||
|
className: string;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
text: string;
|
||||||
|
images: string[];
|
||||||
|
attachments: string[];
|
||||||
|
grade: GradeResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批改入参 */
|
||||||
|
export interface GradeRequest {
|
||||||
|
subId: number;
|
||||||
|
/** grade 保存批改 / redo 要求订正 / excellent 标为优秀 */
|
||||||
|
action?: "grade" | "redo" | "excellent";
|
||||||
|
score?: number | string;
|
||||||
|
level?: string;
|
||||||
|
comment?: string;
|
||||||
|
isExcellent?: boolean;
|
||||||
|
markedImages?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 请求入参 ============================
|
||||||
|
|
||||||
|
/** 作业发布/编辑入参(对齐 HomeworkRequest.java) */
|
||||||
|
export interface HomeworkRequest {
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
/** 学科 */
|
||||||
|
subject?: string;
|
||||||
|
/** 截止时间 yyyy-MM-dd HH:mm:ss */
|
||||||
|
deadline?: string;
|
||||||
|
/** 提交方式:text/image/attach/none */
|
||||||
|
submitType?: string[] | string;
|
||||||
|
type?: number;
|
||||||
|
deptId?: string;
|
||||||
|
/** 是否允许逾期补交 */
|
||||||
|
allowLate?: boolean;
|
||||||
|
/** 截止提醒:如 ["1d","1h"] */
|
||||||
|
remindBefore?: string[];
|
||||||
|
/** 存草稿:true 则不发布 */
|
||||||
|
draft?: boolean;
|
||||||
|
deptName?: string;
|
||||||
|
topmost?: number;
|
||||||
|
/** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */
|
||||||
|
receiveList?: string;
|
||||||
|
/** 接收对象类型:class=班级 */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 附件文件ID列表(逗号分隔串) */
|
||||||
|
fileIds?: string;
|
||||||
|
timingSendTime?: string;
|
||||||
|
/** 短信发送标记 */
|
||||||
|
smsFlag?: number;
|
||||||
|
/** 转发源作业ID */
|
||||||
|
sourceId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业列表查询参数(对齐 HomeworkQuery.java + BasePageQuery.java) */
|
||||||
|
export interface HomeworkQuery {
|
||||||
|
pageIndex?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
|
status?: number;
|
||||||
|
type?: number;
|
||||||
|
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
||||||
|
scope?: string;
|
||||||
|
deptId?: string;
|
||||||
|
/** 学科筛选 */
|
||||||
|
subject?: string;
|
||||||
|
/** 作业流程状态筛选:draft/collecting/closed/done,缺省或 all=全部 */
|
||||||
|
hwStatus?: string;
|
||||||
|
publisherAccount?: string;
|
||||||
|
/** 开始时间(yyyy-MM-dd 短日期) */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束时间(yyyy-MM-dd 短日期) */
|
||||||
|
endTime?: string;
|
||||||
|
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||||
|
readType?: number;
|
||||||
|
/** 排序:1 时间倒序 / 2 时间正序 */
|
||||||
|
orderByTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 字典/联系组 ============================
|
||||||
|
|
||||||
|
/** 系统字典下拉项 */
|
||||||
|
export interface DictOption {
|
||||||
|
k: number | string;
|
||||||
|
v: string | null;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发文部门管理列表项 */
|
||||||
|
export interface DeptManageItem {
|
||||||
|
SerID: number;
|
||||||
|
CodeID: string;
|
||||||
|
Name: string;
|
||||||
|
Enable: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组列表项 */
|
||||||
|
export interface ContactGroupItem {
|
||||||
|
serid: number;
|
||||||
|
GroupName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组分页结果 */
|
||||||
|
export interface ContactGroupPage {
|
||||||
|
list: ContactGroupItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组用户项 */
|
||||||
|
export interface ContactUser {
|
||||||
|
account: string;
|
||||||
|
name: string;
|
||||||
|
userId?: number;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组用户分页结果 */
|
||||||
|
export interface ContactUserPage {
|
||||||
|
list: ContactUser[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组织树节点(基础平台返回) */
|
||||||
|
export interface SchoolTreeNode {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 班级 ============================
|
||||||
|
|
||||||
|
/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */
|
||||||
|
export interface ClassTreeNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isParent: boolean;
|
||||||
|
studentCount?: number;
|
||||||
|
children?: ClassTreeNode[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ v2 学生端 ============================
|
||||||
|
|
||||||
|
/** 当前视角角色(家长即登录学生账号,不单列) */
|
||||||
|
export type ViewerRole = "teacher" | "student";
|
||||||
|
|
||||||
|
/** 学生对某作业的提交/批改态 */
|
||||||
|
export interface StudentSubmissionInfo {
|
||||||
|
subId: number;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
text: string;
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
comment: string;
|
||||||
|
isExcellent: boolean;
|
||||||
|
images?: string[];
|
||||||
|
attachments?: string[];
|
||||||
|
grade?: GradeResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学生视角作业项 */
|
||||||
|
export interface StudentHomeworkVo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
subject: string;
|
||||||
|
publisherName: string;
|
||||||
|
deptName: string;
|
||||||
|
deadline: string | null;
|
||||||
|
createTime: string | null;
|
||||||
|
content: string;
|
||||||
|
submitType: string[];
|
||||||
|
viewOnly: boolean;
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
allowLate?: boolean;
|
||||||
|
my: StudentSubmissionInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 今日/我的作业列表 */
|
||||||
|
export interface MyHomeworksVo {
|
||||||
|
buckets: { todo: number; submitted: number; graded: number; redo: number; all: number };
|
||||||
|
list: StudentHomeworkVo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的某作业详情(作业信息 + 我的提交/批改) */
|
||||||
|
export interface MySubmissionVo {
|
||||||
|
homework: { id: number; title: string; subject: string; content: string; deadline: string | null; submitType: string[]; publisherName: string; deptName: string };
|
||||||
|
my: StudentSubmissionInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学生提交作业入参 */
|
||||||
|
export interface SubmitRequest {
|
||||||
|
serid: number;
|
||||||
|
text?: string;
|
||||||
|
/** 图片 URL / dataURL 列表(逗号串或数组) */
|
||||||
|
images?: string[] | string;
|
||||||
|
/** 附件 id 列表(逗号串或数组) */
|
||||||
|
attachments?: string[] | string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||||
|
import TypeSetBox from "@/components/TypeSetBox.vue";
|
||||||
|
import { settingApi, baseApi } from "@/api/module/homework";
|
||||||
|
import type { DeptManageItem, ContactGroupPage, ContactUserPage, SchoolTreeNode } from "@/types/homework";
|
||||||
|
import Utils from "@/utils";
|
||||||
|
|
||||||
|
/* ============ Tab 切换(规范 §1:tabs-wrapper + Tabs type=card,切换只 v-show 内容区) ============ */
|
||||||
|
const currentType = ref(1);
|
||||||
|
|
||||||
|
function tabChange(name: string) {
|
||||||
|
currentType.value = Number(name);
|
||||||
|
if (currentType.value === 1) {
|
||||||
|
getDeptList();
|
||||||
|
} else if (currentType.value === 3) {
|
||||||
|
getGroupList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 发文部门(直接复用 UserBase 同源 TypeSetBox 卡片组件) ============ */
|
||||||
|
interface DeptItem {
|
||||||
|
SerID: number;
|
||||||
|
CodeID: string;
|
||||||
|
Name: string;
|
||||||
|
Enable: number;
|
||||||
|
}
|
||||||
|
const deptData = ref<DeptItem[]>([]);
|
||||||
|
const deptModal = ref(false);
|
||||||
|
const deptForm = reactive({ name: "" });
|
||||||
|
const deptFormRef = ref();
|
||||||
|
const deptRule = {
|
||||||
|
name: [{ required: true, message: "请输入发文部门名称", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getDeptList() {
|
||||||
|
try {
|
||||||
|
const data: DeptManageItem[] = await settingApi.getDictMangeList().send();
|
||||||
|
deptData.value = (data || []).map((d) => ({
|
||||||
|
SerID: d.SerID,
|
||||||
|
CodeID: d.CodeID,
|
||||||
|
Name: d.Name,
|
||||||
|
Enable: d.Enable ?? 1,
|
||||||
|
}));
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取发文部门列表失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDeptShow() {
|
||||||
|
deptForm.name = "";
|
||||||
|
deptModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deptModalSave() {
|
||||||
|
const valid = await deptFormRef.value?.validate();
|
||||||
|
if (!valid) {
|
||||||
|
Message.info("请填写红色星号内容!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await settingApi.addDeptDic(deptForm.name.trim()).send();
|
||||||
|
Message.success("操作成功!");
|
||||||
|
deptModal.value = false;
|
||||||
|
await getDeptList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDept(id: string): DeptItem | undefined {
|
||||||
|
return deptData.value.find((x) => String(x.SerID) === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TypeSetBox 启用/停用(保留确认弹窗) */
|
||||||
|
function onDeptToggle(id: string) {
|
||||||
|
const item = findDept(id);
|
||||||
|
if (item) toggleDeptStatus(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TypeSetBox 内联编辑部门名(组件已拦截空值) */
|
||||||
|
async function onDeptRename(id: string, name: string) {
|
||||||
|
const item = findDept(id);
|
||||||
|
if (!item) return;
|
||||||
|
try {
|
||||||
|
await settingApi.updateDeptDic(item.SerID, name.trim(), item.Enable).send();
|
||||||
|
Message.success("操作成功!");
|
||||||
|
await getDeptList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeptRemove(id: string) {
|
||||||
|
const item = findDept(id);
|
||||||
|
if (item) deleteDept(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDept(item: DeptItem) {
|
||||||
|
await MyConfirm({
|
||||||
|
title: "删除",
|
||||||
|
msg: `确定删除“ ${item.Name} ”这个发文部门吗?`,
|
||||||
|
info: "删除后不可恢复",
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await settingApi.delDept(item.SerID).send();
|
||||||
|
Message.success("操作成功!");
|
||||||
|
await getDeptList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleDeptStatus(item: DeptItem) {
|
||||||
|
const enable = item.Enable === 1;
|
||||||
|
await MyConfirm({
|
||||||
|
msg: `确定${enable ? "停用" : "启用"}发文部门「${item.Name}」吗?`,
|
||||||
|
title: "修改状态",
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await settingApi.updateDeptDic(item.SerID, item.Name, enable ? 0 : 1).send();
|
||||||
|
item.Enable = enable ? 0 : 1;
|
||||||
|
Message.success("操作成功!");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动排序:提交当前顺序的 SerID 数组 */
|
||||||
|
async function deptSortSave() {
|
||||||
|
const ids = deptData.value.map((x: DeptItem) => x.SerID);
|
||||||
|
try {
|
||||||
|
await settingApi.sortDeptDic(ids.join(",")).send();
|
||||||
|
Message.success("排序成功");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "排序失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 自定义组(规范 §5 联系组管理页形态) ============ */
|
||||||
|
interface GroupItem {
|
||||||
|
serid: number;
|
||||||
|
GroupName: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
const groupData = ref<GroupItem[]>([]);
|
||||||
|
const groupTotal = ref(0);
|
||||||
|
const groupPage = ref(1);
|
||||||
|
const groupCount = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
|
||||||
|
|
||||||
|
async function getGroupList() {
|
||||||
|
try {
|
||||||
|
const data: any = await settingApi.getGroup(groupPage.value, groupCount.value).send();
|
||||||
|
groupData.value = (data.list || []).map((d: any) => ({
|
||||||
|
serid: d.serid,
|
||||||
|
GroupName: d.GroupName,
|
||||||
|
count: d.count ?? 0,
|
||||||
|
}));
|
||||||
|
groupTotal.value = data.total || 0;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取自定义组列表失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupModal = ref(false);
|
||||||
|
const groupEditingId = ref<number | null>(null);
|
||||||
|
const groupForm = reactive({ name: "" });
|
||||||
|
const groupFormRef = ref();
|
||||||
|
const groupRule = {
|
||||||
|
name: [{ required: true, message: "请输入联系组名称", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
function addGroupShow() {
|
||||||
|
groupEditingId.value = null;
|
||||||
|
groupForm.name = "";
|
||||||
|
groupModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editGroupShow(row: GroupItem) {
|
||||||
|
groupEditingId.value = row.serid;
|
||||||
|
groupForm.name = row.GroupName;
|
||||||
|
groupModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function groupModalSave() {
|
||||||
|
const valid = await groupFormRef.value?.validate();
|
||||||
|
if (!valid) {
|
||||||
|
Message.info("请填写红色星号内容!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (groupEditingId.value == null) {
|
||||||
|
await settingApi.addGroup(groupForm.name.trim()).send();
|
||||||
|
} else {
|
||||||
|
await settingApi.editGroup(groupEditingId.value, groupForm.name.trim()).send();
|
||||||
|
}
|
||||||
|
Message.success("操作成功!");
|
||||||
|
groupModal.value = false;
|
||||||
|
await getGroupList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGroup(row: GroupItem) {
|
||||||
|
await MyConfirm({
|
||||||
|
title: "删除",
|
||||||
|
msg: `确定删除“ ${row.GroupName} ”这个组吗?`,
|
||||||
|
info: "删除后不可恢复",
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await settingApi.delGroup(String(row.serid)).send();
|
||||||
|
Message.success("操作成功!");
|
||||||
|
// 删除后智能翻页:当前页仅剩一条则回退一页
|
||||||
|
if (groupData.value.length <= 1 && groupPage.value > 1) groupPage.value -= 1;
|
||||||
|
await getGroupList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 查看联系组用户 ---------- */
|
||||||
|
const seeUserModal = ref(false);
|
||||||
|
const seeUserGid = ref(0);
|
||||||
|
const seeUserGroupName = ref("");
|
||||||
|
const seeUserData = ref<{ account: string; name: string }[]>([]);
|
||||||
|
const seeUserTotal = ref(0);
|
||||||
|
const seeUserPage = ref(1);
|
||||||
|
const seeUserColumns = [
|
||||||
|
{ title: "序号", type: "index", width: 70 },
|
||||||
|
{ title: "账号", key: "account", align: "center" },
|
||||||
|
{ title: "真实姓名", key: "name", width: 160, align: "center" },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function seeUserShow(row: GroupItem) {
|
||||||
|
seeUserGid.value = row.serid;
|
||||||
|
seeUserGroupName.value = row.GroupName;
|
||||||
|
seeUserPage.value = 1;
|
||||||
|
await loadSeeUser();
|
||||||
|
seeUserModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSeeUser() {
|
||||||
|
try {
|
||||||
|
const data: ContactUserPage = await settingApi.getContactsTea(seeUserGid.value, seeUserPage.value, 5).send();
|
||||||
|
seeUserData.value = (data.list || []).map((d) => ({ account: d.account, name: d.name }));
|
||||||
|
seeUserTotal.value = data.total || 0;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取用户列表失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 设置联系组用户(规范选人组件,打开前回显已有成员) ---------- */
|
||||||
|
const setUserCheckRef = ref();
|
||||||
|
const setUserGid = ref(0);
|
||||||
|
const setUserIds = ref<number[]>([]);
|
||||||
|
// 设置自定义组成员只需要基础平台组织树,无需「基础平台/自定义组」两个 tab
|
||||||
|
const setUserSources = [
|
||||||
|
{
|
||||||
|
name: "基础平台",
|
||||||
|
loader: () => baseApi.getSchoolAll().send(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function settingUserShow(row: GroupItem) {
|
||||||
|
setUserGid.value = row.serid;
|
||||||
|
setUserIds.value = [];
|
||||||
|
try {
|
||||||
|
const data: ContactUserPage = await settingApi.getContactsTea(row.serid, 0, 0).send();
|
||||||
|
setUserIds.value = (data.list || [])
|
||||||
|
.map((d) => Number(d.userId ?? d.id))
|
||||||
|
.filter((n: number) => Number.isFinite(n) && n > 0);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取已有用户失败");
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
setUserCheckRef.value.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settingUserSubmit(users: SchoolTreeNode[]) {
|
||||||
|
if (!users.length) {
|
||||||
|
Message.info("请选择用户!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// on-submit 返回的是用户对象数组({id,name,group}),取 id 拼接
|
||||||
|
const ids = users.map((x) => (typeof x === "object" && x != null ? x.id : x)).join(",");
|
||||||
|
try {
|
||||||
|
await settingApi.setContactsTea(setUserGid.value, ids).send();
|
||||||
|
Message.success("设置成功");
|
||||||
|
// 刷新组列表,组员数即时更新
|
||||||
|
await getGroupList();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "设置失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDeptList();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 自定义组表格(规范 §5:序号/组名/操作图标列 width 250) ---------- */
|
||||||
|
const groupColumns = [
|
||||||
|
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||||
|
{ title: "组名", key: "GroupName", align: "left" },
|
||||||
|
{ title: "组员数", key: "count", width: 100, align: "center" },
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
width: 250,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, params: any) => {
|
||||||
|
const row: GroupItem = params.row;
|
||||||
|
return [
|
||||||
|
Utils.operatingHandle(h, "查看用户", "icon-browse", () => seeUserShow(row)),
|
||||||
|
Utils.operatingHandle(h, "编辑", "icon-edit", () => editGroupShow(row)),
|
||||||
|
Utils.operatingHandle(h, "删除", "icon-ashbin", () => deleteGroup(row)),
|
||||||
|
Utils.operatingHandle(h, "设置用户", "icon-user-filling", () => settingUserShow(row)),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isSearch: true, isPage: true }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tabs-wrapper">
|
||||||
|
<Tabs class="tabs-panel" type="card" :value="String(currentType)" @on-click="tabChange">
|
||||||
|
<TabPane label="发文部门" name="1"></TabPane>
|
||||||
|
<TabPane label="自定义组" name="3"></TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<!-- 发文部门 -->
|
||||||
|
<div class="main-container" v-show="currentType === 1">
|
||||||
|
<Alert show-icon>设置说明:拖动卡片调整部门排序;停用的部门不出现在「作业发送」的部门下拉中</Alert>
|
||||||
|
<div class="search-wrap fix">
|
||||||
|
<Button class="fl" type="primary" @click="addDeptShow">新增部门</Button>
|
||||||
|
</div>
|
||||||
|
<draggable
|
||||||
|
class="type_set-wrap fix"
|
||||||
|
tag="ul"
|
||||||
|
v-model="deptData"
|
||||||
|
group="homeworkDept"
|
||||||
|
item-key="SerID"
|
||||||
|
@change="deptSortSave"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<li>
|
||||||
|
<TypeSetBox
|
||||||
|
:type="{ id: String(element.SerID), name: element.Name }"
|
||||||
|
:active="element.Enable === 1"
|
||||||
|
@active="onDeptToggle"
|
||||||
|
@edit="onDeptRename"
|
||||||
|
@remove="onDeptRemove"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义组 -->
|
||||||
|
<div class="main-container" v-show="currentType === 3">
|
||||||
|
<div class="search-wrap fix">
|
||||||
|
<Button class="fl" type="primary" @click="addGroupShow">新建联系组</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
:columns="groupColumns"
|
||||||
|
:data="groupData"
|
||||||
|
class="link-table table-bottom-box"
|
||||||
|
:height="groupTableHeight"
|
||||||
|
no-data-text="暂无数据"
|
||||||
|
></Table>
|
||||||
|
<div class="page-panel">
|
||||||
|
<div class="fl ml-15 align-middle"></div>
|
||||||
|
<Page
|
||||||
|
class="page-box fr"
|
||||||
|
:total="groupTotal"
|
||||||
|
:current="groupPage"
|
||||||
|
:page-size="groupCount"
|
||||||
|
:page-size-opts="[10, 20, 30, 40]"
|
||||||
|
@on-page-size-change="
|
||||||
|
(size: number) => {
|
||||||
|
groupPage = 1;
|
||||||
|
groupCount = size;
|
||||||
|
localStorage.setItem('customPageCount', String(size));
|
||||||
|
getGroupList();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@on-change="
|
||||||
|
(p: number) => {
|
||||||
|
groupPage = p;
|
||||||
|
getGroupList();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
placement="top"
|
||||||
|
show-total
|
||||||
|
show-sizer
|
||||||
|
></Page>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增发文部门 Modal(编辑走 TypeSetBox 内联编辑) -->
|
||||||
|
<Modal v-model="deptModal" :mask-closable="false" width="450px" title="新增部门">
|
||||||
|
<Form ref="deptFormRef" :model="deptForm" :rules="deptRule" :label-width="110">
|
||||||
|
<FormItem label="发文部门名称:" prop="name">
|
||||||
|
<Input placeholder="请输入发文部门名称" v-model="deptForm.name" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button type="primary" @click="deptModalSave">提交</Button>
|
||||||
|
<Button @click="deptModal = false">取消</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 新建/编辑联系组 Modal(规范 §5:450px + label-width 100 + 提交/取消) -->
|
||||||
|
<Modal v-model="groupModal" :mask-closable="false" width="450px" :title="groupEditingId == null ? '新建联系组' : '编辑联系组'">
|
||||||
|
<Form ref="groupFormRef" :model="groupForm" :rules="groupRule" :label-width="100">
|
||||||
|
<FormItem label="联系组名称:" prop="name">
|
||||||
|
<Input placeholder="请输入联系组名称" v-model="groupForm.name" />
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button type="primary" @click="groupModalSave">提交</Button>
|
||||||
|
<Button @click="groupModal = false">取消</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 查看联系组用户 Modal(规范 §5:Modal 内小表格 + small 分页) -->
|
||||||
|
<Modal v-model="seeUserModal" :title="`查看联系组用户${seeUserGroupName ? ' - ' + seeUserGroupName : ''}`">
|
||||||
|
<Table height="260" :columns="seeUserColumns" :data="seeUserData" no-data-text="暂无用户"></Table>
|
||||||
|
<div class="small-pager">
|
||||||
|
<Page
|
||||||
|
:current="seeUserPage"
|
||||||
|
:page-size="5"
|
||||||
|
:total="seeUserTotal"
|
||||||
|
@on-change="
|
||||||
|
(p: number) => {
|
||||||
|
seeUserPage = p;
|
||||||
|
loadSeeUser();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
show-total
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="seeUserModal = false">关闭</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 选择用户弹窗(设置联系组用户) -->
|
||||||
|
<UserCheckModal ref="setUserCheckRef" :sources="setUserSources" :userids="setUserIds" @on-submit="settingUserSubmit" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tab-body {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.type_set-wrap {
|
||||||
|
margin: 12px 0 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
/* 拖动排序手型(与 UserBase AppCategory 页面级样式一致,组件内不含) */
|
||||||
|
.type_set-wrap :deep(.type_name) {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.small-pager {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from "vue";
|
||||||
|
import layout from "./components/layout.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
import { baseApi } from "@/api/module/homework";
|
||||||
|
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const isLogin = ref(false);
|
||||||
|
const layoutData = reactive({
|
||||||
|
appName: "课程作业",
|
||||||
|
baseHost: config.baseHost,
|
||||||
|
ssoHost: config.ssoHost,
|
||||||
|
appList: [] as any[],
|
||||||
|
userInfo: null as any,
|
||||||
|
modList: [] as any[],
|
||||||
|
isAlone: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function roleOfPath(p: string): "teacher" | "student" {
|
||||||
|
if (p.startsWith("/stu")) return "student";
|
||||||
|
return "teacher";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initShell() {
|
||||||
|
const role = roleOfPath(router.currentRoute.value.path);
|
||||||
|
try {
|
||||||
|
// 端分离:先切换 mock 当前视角,再拉身份与菜单
|
||||||
|
await baseApi.setRole(role).send();
|
||||||
|
} catch {
|
||||||
|
/* 忽略:非 mock 环境可能无此接口 */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 1. 获取当前用户信息
|
||||||
|
const user: any = await baseApi.getUserInfo().send();
|
||||||
|
const { id, name, profilePicture } = user || {};
|
||||||
|
|
||||||
|
// 2. 获取当前用户已授权的模块列表
|
||||||
|
const modList: any[] = await baseApi.getUserMod().send();
|
||||||
|
|
||||||
|
layoutData.userInfo = { id, name: name || "管理员", avatar: profilePicture || "" };
|
||||||
|
layoutData.modList = modList || [];
|
||||||
|
|
||||||
|
// 开发环境打印平台原始模块配置,便于排查菜单路径解析问题
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.table(
|
||||||
|
(modList || []).map((m: any) => ({
|
||||||
|
name: m.name,
|
||||||
|
id: m.id,
|
||||||
|
modId: m.modId,
|
||||||
|
path: m.page,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有子菜单时不算「单模块独占」模式
|
||||||
|
const hasChildren = (modList || []).some((m: any) => m.children && m.children.length > 0);
|
||||||
|
layoutData.isAlone = (modList || []).length <= 1 && !hasChildren;
|
||||||
|
|
||||||
|
isLogin.value = true;
|
||||||
|
|
||||||
|
// 登录后跳转第一个可用模块("/" 本身即已收到页,无需跳转)
|
||||||
|
if (router.currentRoute.value.path === "/") {
|
||||||
|
const firstModule = (modList || [])[0];
|
||||||
|
const firstRaw = firstModule?.children?.length
|
||||||
|
? firstModule.children[0]
|
||||||
|
: firstModule;
|
||||||
|
const first = resolveMenuPage(firstRaw?.page, firstRaw?.modId ?? firstRaw?.id, firstRaw?.name);
|
||||||
|
if (!first.external && first.path && first.path !== "/") {
|
||||||
|
router.replace(first.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("加载用户信息失败", error);
|
||||||
|
if (error?.message !== "redirect") {
|
||||||
|
Message.error("载入用户信息失败,请检查后端服务是否正常!");
|
||||||
|
}
|
||||||
|
// 降级:使用默认值("/" 即已收到页,无需跳转)
|
||||||
|
layoutData.userInfo = { name: "管理员", avatar: "" };
|
||||||
|
layoutData.modList = [];
|
||||||
|
layoutData.isAlone = true;
|
||||||
|
isLogin.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(initShell);
|
||||||
|
|
||||||
|
// 端分离:路由跨角色前缀(/stu、/par、其它)切换时,重新设定视角并初始化壳层
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value.path,
|
||||||
|
(p, prev) => {
|
||||||
|
if (roleOfPath(p) !== roleOfPath(prev || "")) initShell();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||||
|
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* 页面级滚动容器:main-wrapper 只有 min-height 时内容超一屏会被 body 的
|
||||||
|
overflow:hidden 直接裁掉(如发作业页),无任何滚动条。定高 + 纵向滚动,
|
||||||
|
放得下的列表页(表格高度由 Utils.useTableHeight 算好)不受影响 */
|
||||||
|
.base-wrap-main .main-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
/* 表格操作列(operatingHandle 渲染的 Tooltip 包裹层)横排且随单元格居中 */
|
||||||
|
.ivu-table-cell > .ivu-tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import { Method } from "alova";
|
||||||
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
|
import conf from "@/assets/config/host.config";
|
||||||
|
import type {
|
||||||
|
HomeworkVo,
|
||||||
|
HomeworkDetailVo,
|
||||||
|
HomeworkPageResult,
|
||||||
|
BasePageResult,
|
||||||
|
UpFileResultVo,
|
||||||
|
UpFileVo,
|
||||||
|
InitializeRedVo,
|
||||||
|
NextPrevVo,
|
||||||
|
ReadGroup,
|
||||||
|
HomeworkStatisticsVo,
|
||||||
|
HomeworkRequest,
|
||||||
|
HomeworkQuery,
|
||||||
|
DictOption,
|
||||||
|
DeptManageItem,
|
||||||
|
ContactGroupPage,
|
||||||
|
ContactUserPage,
|
||||||
|
SchoolTreeNode,
|
||||||
|
ClassTreeNode,
|
||||||
|
ApprovalQuery,
|
||||||
|
ApproverVo,
|
||||||
|
SubmissionsResultVo,
|
||||||
|
SubmissionDetailVo,
|
||||||
|
GradeRequest,
|
||||||
|
MyHomeworksVo,
|
||||||
|
MySubmissionVo,
|
||||||
|
SubmitRequest,
|
||||||
|
} from "@/types/homework";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。
|
||||||
|
*
|
||||||
|
* 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名;
|
||||||
|
* platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。
|
||||||
|
* 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。
|
||||||
|
*
|
||||||
|
* 变更要点(P0):
|
||||||
|
* 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List)
|
||||||
|
* 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down)
|
||||||
|
* 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete)
|
||||||
|
* 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)
|
||||||
|
* 5. 参数更名:chaoSend→fileIds、topmost→smsFlag
|
||||||
|
* 6. Delete 入参从 ids 逗号串改为 List<Integer> JSON 数组
|
||||||
|
*
|
||||||
|
* 变更要点(P1-7):
|
||||||
|
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
|
||||||
|
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HOMEWORK = "/api/homework";
|
||||||
|
const BASE = "/api/base";
|
||||||
|
const DICT = "/api/dict";
|
||||||
|
const FILE = "/api/file";
|
||||||
|
const CONTACTS = "/api/contacts";
|
||||||
|
|
||||||
|
/** GET 请求,T 为返回数据类型 */
|
||||||
|
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||||
|
return new Method<T>("GET", alovaInstance, url, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */
|
||||||
|
function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Method<T> {
|
||||||
|
return new Method<T>(
|
||||||
|
"POST",
|
||||||
|
alovaInstance,
|
||||||
|
url,
|
||||||
|
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
|
||||||
|
data ? JSON.stringify(data) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */
|
||||||
|
function postForm<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||||
|
const pairs = data
|
||||||
|
? Object.entries(data).filter(([, v]) => v !== undefined && v !== null)
|
||||||
|
: [];
|
||||||
|
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
||||||
|
// 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收,
|
||||||
|
// 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
|
||||||
|
const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||||
|
return new Method<T>(
|
||||||
|
"POST",
|
||||||
|
alovaInstance,
|
||||||
|
fullUrl,
|
||||||
|
{ headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } },
|
||||||
|
form || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件下载直链(浏览器 location.href 跳转) */
|
||||||
|
function fileDownUrl(fileId: number): string {
|
||||||
|
return `${conf.serviceHost}${FILE}/Down?id=${fileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业全部附件打包下载直链 */
|
||||||
|
function allFileDownUrl(dataid: number): string {
|
||||||
|
return `${conf.serviceHost}${HOMEWORK}/AllFileDown?dataid=${dataid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseApi {
|
||||||
|
// ========== 用户信息 & 模块权限 ==========
|
||||||
|
/** 获取当前登录用户信息 */
|
||||||
|
getUserInfo() {
|
||||||
|
return get<SchoolTreeNode>(`${BASE}/GetUserInfo`);
|
||||||
|
}
|
||||||
|
/** 获取当前用户已授权的模块列表 */
|
||||||
|
getUserMod() {
|
||||||
|
return get<SchoolTreeNode[]>(`${BASE}/GetUserMod`);
|
||||||
|
}
|
||||||
|
/** 当前学校组织树(部门→教师,用于选择用户) */
|
||||||
|
getSchoolAll() {
|
||||||
|
return get<SchoolTreeNode[]>(`${BASE}/GetSchoolAll`);
|
||||||
|
}
|
||||||
|
/** 年级→班级树(学生场景:选择通知对象到班级一级) */
|
||||||
|
getClassTree() {
|
||||||
|
return get<ClassTreeNode[]>(`${BASE}/GetClassTree`);
|
||||||
|
}
|
||||||
|
/** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */
|
||||||
|
setRole(as: "teacher" | "student" | "parent") {
|
||||||
|
return postJSON<{ role: string }>(`${BASE}/SetRole`, { as });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeworkApi {
|
||||||
|
// ========== 收件箱 ==========
|
||||||
|
/** 已收作业列表 */
|
||||||
|
list(params: HomeworkQuery) {
|
||||||
|
return get<HomeworkPageResult<HomeworkVo>>(`${HOMEWORK}/List`, params);
|
||||||
|
}
|
||||||
|
/** 清单:一键全部已读 */
|
||||||
|
initializeRed() {
|
||||||
|
return postForm<InitializeRedVo>(`${HOMEWORK}/InitializeRed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 已发/全部作业 ==========
|
||||||
|
/** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */
|
||||||
|
sendList(params: HomeworkQuery) {
|
||||||
|
return get<BasePageResult<HomeworkVo>>(`${HOMEWORK}/SendList`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 详情 ==========
|
||||||
|
/** 查看详情(type=1 标读;type=0 编辑不标读) */
|
||||||
|
modify(serid: number, type = 1) {
|
||||||
|
return get<HomeworkDetailVo>(`${HOMEWORK}/Detail`, { serid, type });
|
||||||
|
}
|
||||||
|
/** 编辑回显(不标读) */
|
||||||
|
getHomeworkDetails(serid: number) {
|
||||||
|
return get<HomeworkDetailVo>(`${HOMEWORK}/GetDetail`, { serid });
|
||||||
|
}
|
||||||
|
/** 上一篇/下一篇 */
|
||||||
|
getNextPrevHomework(serid: number, type: string) {
|
||||||
|
return get<NextPrevVo>(`${HOMEWORK}/GetNextPrev`, { serid, type });
|
||||||
|
}
|
||||||
|
/** 阅读情况(已读/未读分组) */
|
||||||
|
orgizeModify(serid: number) {
|
||||||
|
return get<ReadGroup[]>(`${HOMEWORK}/ReadDetail`, { serid });
|
||||||
|
}
|
||||||
|
/** 追加接收人 */
|
||||||
|
receiveUsers(serid: number, users: string) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/ReceiveUsers`, { serid, users });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 发布/编辑/删除 ==========
|
||||||
|
/** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */
|
||||||
|
create(data: HomeworkRequest) {
|
||||||
|
return postJSON<number>(`${HOMEWORK}/Create`, data);
|
||||||
|
}
|
||||||
|
/** 编辑作业(JSON body,含 id) */
|
||||||
|
editHomework(data: HomeworkRequest) {
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Edit`, data);
|
||||||
|
}
|
||||||
|
/** 删除作业(ids 数组,支持批量) */
|
||||||
|
deleteHomework(ids: number | string) {
|
||||||
|
// 兼容页面传单个 id 或逗号串,统一转成数组
|
||||||
|
const idArr = String(ids).split(",").map(Number).filter(Boolean);
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Delete`, idArr);
|
||||||
|
}
|
||||||
|
/** 撤回 */
|
||||||
|
userPull(serid: number) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/UserPull`, { serid });
|
||||||
|
}
|
||||||
|
/** 置顶/取消置顶 */
|
||||||
|
setTopmost(serid: number, topmost = 1) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/SetTopmost`, { serid, topmost });
|
||||||
|
}
|
||||||
|
/** 催收未读 */
|
||||||
|
expedite(serid: number) {
|
||||||
|
return postForm<void>(`${HOMEWORK}/Expedite`, { serid });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 课程作业 v1:提交情况 / 批改 / 催交 ==========
|
||||||
|
/** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */
|
||||||
|
submissions(serid: number, submitStatus = "all") {
|
||||||
|
return get<SubmissionsResultVo>(`${HOMEWORK}/Submissions`, { serid, submitStatus });
|
||||||
|
}
|
||||||
|
/** 单份提交详情(批改工作台右侧) */
|
||||||
|
submissionDetail(subId: number) {
|
||||||
|
return get<SubmissionDetailVo>(`${HOMEWORK}/SubmissionDetail`, { subId });
|
||||||
|
}
|
||||||
|
/** 批改(JSON body):action=grade/redo/excellent */
|
||||||
|
grade(data: GradeRequest) {
|
||||||
|
return postJSON<void>(`${HOMEWORK}/Grade`, data);
|
||||||
|
}
|
||||||
|
/** 催交未交学生 */
|
||||||
|
remind(serid: number) {
|
||||||
|
return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== v2 学生端 ==========
|
||||||
|
/** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */
|
||||||
|
myHomeworks() {
|
||||||
|
return get<MyHomeworksVo>(`${HOMEWORK}/MyHomeworks`);
|
||||||
|
}
|
||||||
|
/** 我对某作业的详情(作业信息 + 我的提交/批改) */
|
||||||
|
mySubmission(serid: number) {
|
||||||
|
return get<MySubmissionVo>(`${HOMEWORK}/MySubmission`, { serid });
|
||||||
|
}
|
||||||
|
/** 提交 / 订正重交(JSON body:serid, text, images, attachments) */
|
||||||
|
submit(data: SubmitRequest) {
|
||||||
|
return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 附件 ==========
|
||||||
|
fileDownUrl = fileDownUrl;
|
||||||
|
allFileDownUrl = allFileDownUrl;
|
||||||
|
|
||||||
|
// ========== 部门字典 ==========
|
||||||
|
/** 发文部门下拉 */
|
||||||
|
getSysDic() {
|
||||||
|
return get<DictOption[]>(`${DICT}/List`, { type: 1 });
|
||||||
|
}
|
||||||
|
/** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */
|
||||||
|
getSchModuleIsSms() {
|
||||||
|
return get<boolean>(`${DICT}/Sync`);
|
||||||
|
}
|
||||||
|
/** 作业统计 */
|
||||||
|
statistics(params?: { starttime?: string; endtime?: string }) {
|
||||||
|
return get<HomeworkStatisticsVo>(`${HOMEWORK}/Statis`, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingApi {
|
||||||
|
// ========== 发文部门管理 ==========
|
||||||
|
/** 获取发文部门管理列表 */
|
||||||
|
getDictMangeList() {
|
||||||
|
return get<DeptManageItem[]>(`${DICT}/ManageList`, { type: "dept" });
|
||||||
|
}
|
||||||
|
/** 新增发文部门 */
|
||||||
|
addDeptDic(name: string) {
|
||||||
|
return postForm<void>(`${DICT}/Add`, { name });
|
||||||
|
}
|
||||||
|
/** 更新发文部门(名称、启用状态) */
|
||||||
|
updateDeptDic(SerID: number, Name: string, Enable = 1) {
|
||||||
|
return postForm<void>(`${DICT}/Update`, { SerID, Name, Enable });
|
||||||
|
}
|
||||||
|
/** 删除发文部门(原 GET DelDept,已 POST 化) */
|
||||||
|
delDept(serid: number) {
|
||||||
|
return postForm<void>(`${DICT}/Delete`, { serid });
|
||||||
|
}
|
||||||
|
/** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */
|
||||||
|
sortDeptDic(ids: string) {
|
||||||
|
return postForm<void>(`${DICT}/Sort`, { ids });
|
||||||
|
}
|
||||||
|
// ========== 自定义组管理 ==========
|
||||||
|
/** 获取联系组列表 */
|
||||||
|
getGroup(page: number, count: number) {
|
||||||
|
return get<ContactGroupPage>(`${CONTACTS}/GetGroup`, { page, count });
|
||||||
|
}
|
||||||
|
/** 新增联系组 */
|
||||||
|
addGroup(name: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/AddGroup`, { name });
|
||||||
|
}
|
||||||
|
/** 编辑联系组 */
|
||||||
|
editGroup(id: number, name: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/EditGroup`, { name, id });
|
||||||
|
}
|
||||||
|
/** 删除联系组 */
|
||||||
|
delGroup(ids: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/DelGroup`, { ids });
|
||||||
|
}
|
||||||
|
/** 获取联系组用户 */
|
||||||
|
getContactsTea(gid: number, page = 0, count = 0) {
|
||||||
|
return get<ContactUserPage>(`${CONTACTS}/GetContactsTea`, { gid, page, count });
|
||||||
|
}
|
||||||
|
/** 设置联系组用户 */
|
||||||
|
setContactsTea(gid: number, ids: string) {
|
||||||
|
return postForm<void>(`${CONTACTS}/SetContactsTea`, { gid, ids });
|
||||||
|
}
|
||||||
|
/** 联系组用户树(选择用户-自定义组 tab) */
|
||||||
|
getGroupTree() {
|
||||||
|
return get<SchoolTreeNode[]>(`${CONTACTS}/GetGroupTree`, { type: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseApi = new BaseApi();
|
||||||
|
export const homeworkApi = new HomeworkApi();
|
||||||
|
export const settingApi = new SettingApi();
|
||||||
|
export default homeworkApi;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { IModule } from "@/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。
|
||||||
|
* 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。
|
||||||
|
*/
|
||||||
|
export function getMenuList(): Promise<IModule[]> {
|
||||||
|
const menu: IModule[] = [
|
||||||
|
{ id: 1, modId: 1, name: "已收到", page: "/" },
|
||||||
|
{ id: 2, modId: 2, name: "已发送", page: "/list" },
|
||||||
|
{ id: 3, modId: 3, name: "全部作业", page: "/homeworklist" },
|
||||||
|
{ id: 5, modId: 5, name: "审批列表", page: "/approval" },
|
||||||
|
{ id: 6, modId: 6, name: "审核设置", page: "/approval-setting" },
|
||||||
|
{ id: 4, modId: 4, name: "作业统计", page: "/stats" },
|
||||||
|
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
|
||||||
|
];
|
||||||
|
return Promise.resolve(menu);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,575 @@
|
|||||||
|
/**
|
||||||
|
* 课程作业前端类型契约(P1-7)
|
||||||
|
*
|
||||||
|
* 与后端 VO/DTO 一一对齐:
|
||||||
|
* - HomeworkVo → 后端 model/homework/HomeworkVo.java(Jackson 默认驼峰序列化)
|
||||||
|
* - HomeworkDetailVo → 后端 model/homework/HomeworkDetailVo.java(@JsonProperty 保留旧字段名)
|
||||||
|
* - BasePageResult → 后端 model/BasePageResult.java
|
||||||
|
* - HomeworkPageResult extends BasePageResult → 后端 model/homework/HomeworkPageResult.java
|
||||||
|
* - UpFileResultVo → 后端 model/homework/UpFileResultVo.java(@JsonProperty)
|
||||||
|
* - UpFileVo → 后端 model/homework/UpFileVo.java
|
||||||
|
* - InitializeRedVo → 后端 model/homework/InitializeRedVo.java(@JsonProperty)
|
||||||
|
* - HomeworkStatisticsVo → 后端 model/homework/HomeworkStatisticsVo.java
|
||||||
|
* - HomeworkRequest → 后端 model/homework/HomeworkRequest.java
|
||||||
|
* - HomeworkQuery → 后端 model/homework/HomeworkQuery.java + BasePageQuery.java
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================ 分页外壳 ============================
|
||||||
|
|
||||||
|
/** 统一分页响应外壳(对齐 BasePageResult.java) */
|
||||||
|
export interface BasePageResult<T> {
|
||||||
|
total: number;
|
||||||
|
content: T[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */
|
||||||
|
export interface HomeworkPageResult<T> extends BasePageResult<T> {
|
||||||
|
/** 当前用户未读作业总数 */
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 作业列表项 ============================
|
||||||
|
|
||||||
|
/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */
|
||||||
|
export interface HomeworkVo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
type: number;
|
||||||
|
typeDesc: string;
|
||||||
|
publisherAccount: string;
|
||||||
|
publisherName: string;
|
||||||
|
deptId: string;
|
||||||
|
deptName: string;
|
||||||
|
status: number;
|
||||||
|
statusDesc: string;
|
||||||
|
topmost: number;
|
||||||
|
/** 已读人数 */
|
||||||
|
readCount: number;
|
||||||
|
/** 应读人数 */
|
||||||
|
allowCount: number;
|
||||||
|
/** 当前用户是否已读 */
|
||||||
|
read: boolean;
|
||||||
|
/** 是否撤回:0 否 / 1 是 */
|
||||||
|
isPulled: number;
|
||||||
|
/** 定时发送时间 */
|
||||||
|
timingSendTime: string | null;
|
||||||
|
/** 父作业ID(转发时关联原作业) */
|
||||||
|
parentId: number | null;
|
||||||
|
/** 短信发送标记 */
|
||||||
|
smsFlag: number;
|
||||||
|
/** 接收对象类型:class=班级(学生场景) */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 接收列表:"all" 或 班级 id 逗号串 */
|
||||||
|
receiveList: string | null;
|
||||||
|
/** ===== 课程作业 v1(教师核心闭环)===== */
|
||||||
|
/** 学科 */
|
||||||
|
subject?: string;
|
||||||
|
/** 截止时间 */
|
||||||
|
deadline?: string | null;
|
||||||
|
/** 提交方式:text/image/attach/none */
|
||||||
|
submitType?: string[];
|
||||||
|
/** 作业流程状态:draft 草稿 / collecting 收集中 / closed 已截止 / done 已完成 */
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
hwStatusDesc?: string;
|
||||||
|
/** 提交/批改进度(草稿或仅查阅为 null) */
|
||||||
|
progress?: HomeworkProgress | null;
|
||||||
|
/** 是否启用审批:0 免审直接发布 / 1 需审批后发布 */
|
||||||
|
approvalEnabled: number;
|
||||||
|
/** 审批状态:none 免审 / pending 待审批 / approved 已通过 / rejected 已驳回 */
|
||||||
|
approvalStatus: ApprovalStatus;
|
||||||
|
approvalStatusDesc: string;
|
||||||
|
/** 当前待审步骤下标(从 0 起) */
|
||||||
|
approvalLevel?: number;
|
||||||
|
/** 当前用户是否为该单当前步骤的待签审批人 */
|
||||||
|
canApprove?: boolean;
|
||||||
|
/** 审批意见 */
|
||||||
|
approvalRemark?: string | null;
|
||||||
|
/** 审批人 */
|
||||||
|
approvalBy?: string | null;
|
||||||
|
/** 审批时间 */
|
||||||
|
approvalTime?: string | null;
|
||||||
|
createBy: string | null;
|
||||||
|
createTime: string | null;
|
||||||
|
updateBy: string | null;
|
||||||
|
updateTime: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批状态枚举 */
|
||||||
|
export type ApprovalStatus = "none" | "pending" | "approved" | "rejected";
|
||||||
|
|
||||||
|
// ============================ 作业详情 ============================
|
||||||
|
|
||||||
|
/** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */
|
||||||
|
export interface HomeworkDetailVo {
|
||||||
|
serid: number;
|
||||||
|
title: string;
|
||||||
|
/** 作业内容(旧字段名 Contents) */
|
||||||
|
Contents: string;
|
||||||
|
/** 发布部门ID(旧字段名 DeptID) */
|
||||||
|
DeptID: string;
|
||||||
|
/** 发布部门名称(旧字段名 DeptName) */
|
||||||
|
DeptName: string;
|
||||||
|
/** 发布人姓名(旧字段名 TeaName) */
|
||||||
|
TeaName: string;
|
||||||
|
/** 发布时间(旧字段名 EditDate,已格式化为字符串) */
|
||||||
|
EditDate: string;
|
||||||
|
/** 应读人数 */
|
||||||
|
numcount: number;
|
||||||
|
/** 已读人数 */
|
||||||
|
readnumcount: number;
|
||||||
|
/** 接收人列表 */
|
||||||
|
ReceiveList: string | null;
|
||||||
|
/** 定时发送时间(旧字段名 DsfsTime,已格式化) */
|
||||||
|
DsfsTime: string | null;
|
||||||
|
/** 是否定时发送 */
|
||||||
|
isDsfs: boolean;
|
||||||
|
/** ===== 课程作业 v1 ===== */
|
||||||
|
subject?: string;
|
||||||
|
deadline?: string | null;
|
||||||
|
submitType?: string[];
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
hwStatusDesc?: string;
|
||||||
|
progress?: HomeworkProgress | null;
|
||||||
|
/** 附件列表(旧字段名 ChaoSend) */
|
||||||
|
ChaoSend: HomeworkDetailFileItem[];
|
||||||
|
/** 接收对象列表:"all" 或 班级 id 数组 */
|
||||||
|
tlist: string | string[];
|
||||||
|
/** 接收对象类型:class=班级 */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 接收对象可读文本(全部班级 / 班级名串) */
|
||||||
|
receiveText?: string;
|
||||||
|
/** 是否需审批 */
|
||||||
|
approvalEnabled?: number;
|
||||||
|
/** 审批状态 */
|
||||||
|
approvalStatus?: ApprovalStatus;
|
||||||
|
/** 审批状态文本 */
|
||||||
|
approvalStatusDesc?: string;
|
||||||
|
/** 审批意见 */
|
||||||
|
approvalRemark?: string | null;
|
||||||
|
/** 审批人 */
|
||||||
|
approvalBy?: string | null;
|
||||||
|
/** 审批时间 */
|
||||||
|
approvalTime?: string | null;
|
||||||
|
/** 当前待审步骤下标(从 0 起) */
|
||||||
|
approvalLevel?: number;
|
||||||
|
/** 多步审批流进度(也用于发布/编辑回显审批人配置) */
|
||||||
|
approvalFlow?: {
|
||||||
|
name: string;
|
||||||
|
vote: "all" | "any";
|
||||||
|
/** 审批人账号(编辑回显用) */
|
||||||
|
approvers?: string[];
|
||||||
|
/** 审批人姓名(展示用) */
|
||||||
|
approverNames?: string[];
|
||||||
|
status: "approved" | "pending" | "rejected" | "waiting";
|
||||||
|
records: { account: string; name: string; result: "approved" | "rejected"; time: string; remark: string }[];
|
||||||
|
/** 当前步待签人姓名 */
|
||||||
|
pending: string[];
|
||||||
|
}[];
|
||||||
|
/** 当前用户是否为该单当前步骤的待签审批人 */
|
||||||
|
canApprove?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */
|
||||||
|
export interface HomeworkDetailFileItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
fileid: number;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 文件上传 ============================
|
||||||
|
|
||||||
|
/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */
|
||||||
|
export interface UpFileResultVo {
|
||||||
|
fileid: number;
|
||||||
|
name: string;
|
||||||
|
fileurl: string;
|
||||||
|
host: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件视图对象(对齐 UpFileVo.java) */
|
||||||
|
export interface UpFileVo {
|
||||||
|
id: number;
|
||||||
|
fileTrueName: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: number;
|
||||||
|
fileType: string;
|
||||||
|
topicId: number;
|
||||||
|
topicType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 一键已读 ============================
|
||||||
|
|
||||||
|
/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */
|
||||||
|
export interface InitializeRedVo {
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 上一篇/下一篇 ============================
|
||||||
|
|
||||||
|
/** 上一篇/下一篇作业ID */
|
||||||
|
export interface NextPrevVo {
|
||||||
|
prevId: number;
|
||||||
|
nextId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 阅读情况 ============================
|
||||||
|
|
||||||
|
/** 阅读情况分组项(后端 Map 返回,字段名固定) */
|
||||||
|
export interface ReadGroup {
|
||||||
|
GroupName: string;
|
||||||
|
teaslist: ReadGroupUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 阅读情况用户项(后端 Map 返回) */
|
||||||
|
export interface ReadGroupUser {
|
||||||
|
account: string;
|
||||||
|
teaname: string;
|
||||||
|
states: 'read' | 'unread';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 统计 ============================
|
||||||
|
|
||||||
|
/** 作业统计视图(对齐 HomeworkStatisticsVo.java) */
|
||||||
|
export interface HomeworkStatisticsVo {
|
||||||
|
total: number;
|
||||||
|
publishedCount: number;
|
||||||
|
totalReadRecords: number;
|
||||||
|
unreadForCurrentUser: number;
|
||||||
|
byPublisher: { publisherName: string; count: number }[];
|
||||||
|
byMonth: { year: number; month: number; count: number }[];
|
||||||
|
// ===== 课程作业口径(v1,可选) =====
|
||||||
|
/** 我布置的作业数 */
|
||||||
|
myAssignCount?: number;
|
||||||
|
/** 待批改总量 */
|
||||||
|
pendingGrade?: number;
|
||||||
|
/** 平均提交率(%) */
|
||||||
|
avgSubmitRate?: number;
|
||||||
|
/** 按作业进度 */
|
||||||
|
byHomework?: { id: number; title: string; subject: string; hwStatus: HomeworkStatus } & HomeworkProgress[];
|
||||||
|
/** 分数分布 */
|
||||||
|
scoreDist?: { label: string; key: string; count: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 课程作业 v1:提交 / 批改 ============================
|
||||||
|
|
||||||
|
/** 作业流程状态 */
|
||||||
|
export type HomeworkStatus = "draft" | "collecting" | "closed" | "done";
|
||||||
|
|
||||||
|
/** 单个提交状态 */
|
||||||
|
export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone";
|
||||||
|
|
||||||
|
/** 提交/批改进度 */
|
||||||
|
export interface HomeworkProgress {
|
||||||
|
/** 应交人数 */
|
||||||
|
should: number;
|
||||||
|
/** 已交人数 */
|
||||||
|
submitted: number;
|
||||||
|
/** 已批改人数 */
|
||||||
|
graded: number;
|
||||||
|
/** 待批改人数 */
|
||||||
|
pendingGrade: number;
|
||||||
|
/** 待订正人数 */
|
||||||
|
needRedo: number;
|
||||||
|
/** 已订正人数 */
|
||||||
|
redone: number;
|
||||||
|
/** 未交人数 */
|
||||||
|
unsubmitted: number;
|
||||||
|
/** 提交率(%) */
|
||||||
|
submitRate: number;
|
||||||
|
/** 批改率(%) */
|
||||||
|
gradeRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交名单项(Submissions 接口返回,精简) */
|
||||||
|
export interface SubmissionBrief {
|
||||||
|
subId: number;
|
||||||
|
studentId: number;
|
||||||
|
studentName: string;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
hasGrade: boolean;
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
isExcellent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交名单按班级分组 */
|
||||||
|
export interface SubmissionGroup {
|
||||||
|
className: string;
|
||||||
|
students: SubmissionBrief[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 某作业提交情况结果 */
|
||||||
|
export interface SubmissionsResultVo {
|
||||||
|
homeworkId: number;
|
||||||
|
title: string;
|
||||||
|
subject: string;
|
||||||
|
deadline: string | null;
|
||||||
|
hwStatus: HomeworkStatus;
|
||||||
|
hwStatusDesc: string;
|
||||||
|
summary: HomeworkProgress;
|
||||||
|
groups: SubmissionGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批改结果 */
|
||||||
|
export interface GradeResult {
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
comment: string;
|
||||||
|
isExcellent: boolean;
|
||||||
|
markedImages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单份提交详情(批改工作台用) */
|
||||||
|
export interface SubmissionDetailVo {
|
||||||
|
subId: number;
|
||||||
|
homeworkId: number;
|
||||||
|
homeworkTitle: string;
|
||||||
|
subject: string;
|
||||||
|
studentId: number;
|
||||||
|
studentName: string;
|
||||||
|
classId: string;
|
||||||
|
className: string;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
text: string;
|
||||||
|
images: string[];
|
||||||
|
attachments: string[];
|
||||||
|
grade: GradeResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批改入参 */
|
||||||
|
export interface GradeRequest {
|
||||||
|
subId: number;
|
||||||
|
/** grade 保存批改 / redo 要求订正 / excellent 标为优秀 */
|
||||||
|
action?: "grade" | "redo" | "excellent";
|
||||||
|
score?: number | string;
|
||||||
|
level?: string;
|
||||||
|
comment?: string;
|
||||||
|
isExcellent?: boolean;
|
||||||
|
markedImages?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 请求入参 ============================
|
||||||
|
|
||||||
|
/** 作业发布/编辑入参(对齐 HomeworkRequest.java) */
|
||||||
|
export interface HomeworkRequest {
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
/** 学科 */
|
||||||
|
subject?: string;
|
||||||
|
/** 截止时间 yyyy-MM-dd HH:mm:ss */
|
||||||
|
deadline?: string;
|
||||||
|
/** 提交方式:text/image/attach/none */
|
||||||
|
submitType?: string[] | string;
|
||||||
|
type?: number;
|
||||||
|
deptId?: string;
|
||||||
|
/** 是否允许逾期补交 */
|
||||||
|
allowLate?: boolean;
|
||||||
|
/** 截止提醒:如 ["1d","1h"] */
|
||||||
|
remindBefore?: string[];
|
||||||
|
/** 存草稿:true 则不发布 */
|
||||||
|
draft?: boolean;
|
||||||
|
deptName?: string;
|
||||||
|
topmost?: number;
|
||||||
|
/** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */
|
||||||
|
receiveList?: string;
|
||||||
|
/** 接收对象类型:class=班级 */
|
||||||
|
receiveType?: string;
|
||||||
|
/** 附件文件ID列表(逗号分隔串) */
|
||||||
|
fileIds?: string;
|
||||||
|
timingSendTime?: string;
|
||||||
|
/** 短信发送标记 */
|
||||||
|
smsFlag?: number;
|
||||||
|
/** 审批人账号列表(单步:选人即送审;空=不送审直接发布) */
|
||||||
|
approvers?: string[];
|
||||||
|
/** 单步表决:any=或签(默认)/ all=会签 */
|
||||||
|
vote?: "all" | "any";
|
||||||
|
/** 多步审批流(高级):[{name,vote,approvers}],传了则忽略 approvers/vote */
|
||||||
|
flow?: { name: string; vote: "all" | "any"; approvers: string[] }[];
|
||||||
|
/** 转发源作业ID */
|
||||||
|
sourceId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作业列表查询参数(对齐 HomeworkQuery.java + BasePageQuery.java) */
|
||||||
|
export interface HomeworkQuery {
|
||||||
|
pageIndex?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
|
status?: number;
|
||||||
|
type?: number;
|
||||||
|
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
|
||||||
|
scope?: string;
|
||||||
|
deptId?: string;
|
||||||
|
/** 学科筛选 */
|
||||||
|
subject?: string;
|
||||||
|
/** 作业流程状态筛选:draft/collecting/closed/done,缺省或 all=全部 */
|
||||||
|
hwStatus?: string;
|
||||||
|
publisherAccount?: string;
|
||||||
|
/** 开始时间(yyyy-MM-dd 短日期) */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束时间(yyyy-MM-dd 短日期) */
|
||||||
|
endTime?: string;
|
||||||
|
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||||
|
readType?: number;
|
||||||
|
/** 排序:1 时间倒序 / 2 时间正序 */
|
||||||
|
orderByTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 字典/联系组 ============================
|
||||||
|
|
||||||
|
/** 系统字典下拉项 */
|
||||||
|
export interface DictOption {
|
||||||
|
k: number | string;
|
||||||
|
v: string | null;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发文部门管理列表项 */
|
||||||
|
export interface DeptManageItem {
|
||||||
|
SerID: number;
|
||||||
|
CodeID: string;
|
||||||
|
Name: string;
|
||||||
|
Enable: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组列表项 */
|
||||||
|
export interface ContactGroupItem {
|
||||||
|
serid: number;
|
||||||
|
GroupName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组分页结果 */
|
||||||
|
export interface ContactGroupPage {
|
||||||
|
list: ContactGroupItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组用户项 */
|
||||||
|
export interface ContactUser {
|
||||||
|
account: string;
|
||||||
|
name: string;
|
||||||
|
userId?: number;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 联系组用户分页结果 */
|
||||||
|
export interface ContactUserPage {
|
||||||
|
list: ContactUser[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组织树节点(基础平台返回) */
|
||||||
|
export interface SchoolTreeNode {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 班级 / 审批 ============================
|
||||||
|
|
||||||
|
/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */
|
||||||
|
export interface ClassTreeNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isParent: boolean;
|
||||||
|
studentCount?: number;
|
||||||
|
children?: ClassTreeNode[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批列表查询参数 */
|
||||||
|
export interface ApprovalQuery {
|
||||||
|
pageIndex?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
/** pending / approved / rejected / all(缺省 pending) */
|
||||||
|
status?: string;
|
||||||
|
keyword?: string;
|
||||||
|
/** "user" 只看我提交的审批 */
|
||||||
|
scope?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批操作入参 */
|
||||||
|
export interface ApprovalAction {
|
||||||
|
serid: number;
|
||||||
|
/** 驳回意见 */
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 审批人 ============================
|
||||||
|
|
||||||
|
/** 审批人 */
|
||||||
|
export interface ApproverVo {
|
||||||
|
id: number;
|
||||||
|
account: string;
|
||||||
|
name: string;
|
||||||
|
deptId: string;
|
||||||
|
deptName: string;
|
||||||
|
enabled: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ v2 学生端 ============================
|
||||||
|
|
||||||
|
/** 当前视角角色(家长即登录学生账号,不单列) */
|
||||||
|
export type ViewerRole = "teacher" | "student";
|
||||||
|
|
||||||
|
/** 学生对某作业的提交/批改态 */
|
||||||
|
export interface StudentSubmissionInfo {
|
||||||
|
subId: number;
|
||||||
|
submitStatus: SubmitStatus;
|
||||||
|
submittedAt: string | null;
|
||||||
|
text: string;
|
||||||
|
score: number | null;
|
||||||
|
level: string | null;
|
||||||
|
comment: string;
|
||||||
|
isExcellent: boolean;
|
||||||
|
images?: string[];
|
||||||
|
attachments?: string[];
|
||||||
|
grade?: GradeResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学生视角作业项 */
|
||||||
|
export interface StudentHomeworkVo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
subject: string;
|
||||||
|
publisherName: string;
|
||||||
|
deptName: string;
|
||||||
|
deadline: string | null;
|
||||||
|
createTime: string | null;
|
||||||
|
content: string;
|
||||||
|
submitType: string[];
|
||||||
|
viewOnly: boolean;
|
||||||
|
hwStatus?: HomeworkStatus;
|
||||||
|
allowLate?: boolean;
|
||||||
|
my: StudentSubmissionInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 今日/我的作业列表 */
|
||||||
|
export interface MyHomeworksVo {
|
||||||
|
buckets: { todo: number; submitted: number; graded: number; redo: number; all: number };
|
||||||
|
list: StudentHomeworkVo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的某作业详情(作业信息 + 我的提交/批改) */
|
||||||
|
export interface MySubmissionVo {
|
||||||
|
homework: { id: number; title: string; subject: string; content: string; deadline: string | null; submitType: string[]; publisherName: string; deptName: string };
|
||||||
|
my: StudentSubmissionInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学生提交作业入参 */
|
||||||
|
export interface SubmitRequest {
|
||||||
|
serid: number;
|
||||||
|
text?: string;
|
||||||
|
/** 图片 URL / dataURL 列表(逗号串或数组) */
|
||||||
|
images?: string[] | string;
|
||||||
|
/** 附件 id 列表(逗号串或数组) */
|
||||||
|
attachments?: string[] | string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import routes from "@/router/routes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台模块配置 → 前端内部路由的统一解析器。
|
||||||
|
*
|
||||||
|
* 基础平台里配置的模块 path 格式不可控(可能带 /homework 旧前缀、带
|
||||||
|
* index.html#/hash、是应用入口地址甚至完整 URL、或干脆配错),直接
|
||||||
|
* router.push(item.page) 匹配不到就会白屏或被重定向兜到默认页。所有消费
|
||||||
|
* 模块 page 的地方(侧边栏跳转/高亮、登录后默认跳转)必须先经过这里。
|
||||||
|
*
|
||||||
|
* 解析顺序:路径精确匹配 → 模块名匹配 → 外链判定 → modId 契约匹配。
|
||||||
|
* 全部落空时返回空结果并 console.warn,绝不返回未注册的原始路径——
|
||||||
|
* 把它 push 给路由器会被 / 重定向吞掉,表象就是"点了菜单跳到默认页"。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 路由 path 集合:重定向路由穿透到目标 path,保证解析结果可直接用于高亮对照 */
|
||||||
|
const ROUTE_PATHS = routes
|
||||||
|
.map((r) => (typeof r.redirect === "string" ? r.redirect : r.path))
|
||||||
|
.filter((p): p is string => !!p && !p.includes(":"));
|
||||||
|
|
||||||
|
/** modId → 路由 path(取该 modId 的第一个非重定向路由) */
|
||||||
|
const ROUTE_BY_MODID = new Map<number, string>();
|
||||||
|
for (const r of routes) {
|
||||||
|
const modId = (r.meta as any)?.modId as number | undefined;
|
||||||
|
if (!modId || !r.path || r.path.includes(":") || r.redirect) continue;
|
||||||
|
if (!ROUTE_BY_MODID.has(modId)) ROUTE_BY_MODID.set(modId, r.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标题 → 路由 path(重复标题取先注册的,如 发作业 → /list/new) */
|
||||||
|
const ROUTE_BY_TITLE = new Map<string, string>();
|
||||||
|
for (const r of routes) {
|
||||||
|
if (r.redirect || !r.path || r.path.includes(":")) continue;
|
||||||
|
const title = (r.meta as any)?.title as string | undefined;
|
||||||
|
if (title && !ROUTE_BY_TITLE.has(title)) ROUTE_BY_TITLE.set(title, r.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平台历史模块名 → 路由(旧菜单叫法与现页面标题不一致的对照) */
|
||||||
|
const NAME_ALIASES: Record<string, string> = {
|
||||||
|
作业发送: "/list/new",
|
||||||
|
发作业: "/list/new",
|
||||||
|
已发作业: "/list",
|
||||||
|
已收作业: "/accept",
|
||||||
|
作业列表: "/homeworklist",
|
||||||
|
统计分析: "/stats",
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXTERNAL_RE = /^(https?:)?\/\//i;
|
||||||
|
|
||||||
|
/** 从平台原始配置里提取内部路由候选串(去掉 hash、query、尾斜杠) */
|
||||||
|
function extractCandidate(raw: string): string {
|
||||||
|
let p = raw.trim();
|
||||||
|
try {
|
||||||
|
p = decodeURIComponent(p);
|
||||||
|
} catch {
|
||||||
|
/* 含非法编码时保持原样 */
|
||||||
|
}
|
||||||
|
// 带 hash 的地址(.../index.html#/setting):取 # 后的 SPA 路由
|
||||||
|
const hashIdx = p.indexOf("#");
|
||||||
|
if (hashIdx >= 0) {
|
||||||
|
p = p.slice(hashIdx + 1);
|
||||||
|
} else if (EXTERNAL_RE.test(p)) {
|
||||||
|
// 无 hash 的完整 URL:取 pathname(如 https://host/setting)
|
||||||
|
try {
|
||||||
|
p = new URL(p, "http://placeholder.local").pathname;
|
||||||
|
} catch {
|
||||||
|
/* 保持原样 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qIdx = p.indexOf("?");
|
||||||
|
if (qIdx >= 0) p = p.slice(0, qIdx);
|
||||||
|
// 入口文件名去掉后即可命中根重定向路由
|
||||||
|
p = p.replace(/index\.html?$/i, "");
|
||||||
|
p = p.replace(/\/+$/, "");
|
||||||
|
if (p && !p.startsWith("/")) p = "/" + p;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 候选串 → 已注册路由 path;未命中返回 null */
|
||||||
|
function matchRoute(candidate: string): string | null {
|
||||||
|
if (!candidate) return null;
|
||||||
|
const lower = candidate.toLowerCase();
|
||||||
|
// 1. 精确匹配(含重定向穿透后的目标路径)
|
||||||
|
const exact = ROUTE_PATHS.find((p) => p.toLowerCase() === lower);
|
||||||
|
if (exact) return exact;
|
||||||
|
// 2. 兼容旧 /homework 前缀(/homework/setting → /setting)。
|
||||||
|
// 不做通用后缀匹配,避免 /stats/OA/Homework 这类外部路径被误匹配
|
||||||
|
if (lower.startsWith("/homework/")) {
|
||||||
|
const stripped = lower.slice("/homework".length);
|
||||||
|
return ROUTE_PATHS.find((p) => p.toLowerCase() === stripped) ?? null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按模块名解析路由:精确标题 → 历史别名 → 双向包含(命中多个则放弃) */
|
||||||
|
function matchByName(name?: string | null): string | null {
|
||||||
|
const n = String(name ?? "").trim();
|
||||||
|
if (!n) return null;
|
||||||
|
const byTitle = ROUTE_BY_TITLE.get(n);
|
||||||
|
if (byTitle) return byTitle;
|
||||||
|
const alias = NAME_ALIASES[n];
|
||||||
|
if (alias) return alias;
|
||||||
|
const hits = [...ROUTE_BY_TITLE.entries()].filter(
|
||||||
|
([title]) => title.length >= 2 && (n.includes(title) || title.includes(n))
|
||||||
|
);
|
||||||
|
return hits.length === 1 ? hits[0][1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为本应用入口类地址(/、/homework、…/index.html):只表示"打开应用",
|
||||||
|
* 不携带页面信息,不能当作有效跳转目标 */
|
||||||
|
function isAppEntryCandidate(candidate: string): boolean {
|
||||||
|
return !candidate || candidate === "/" || candidate === "/homework";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedMenuPage {
|
||||||
|
/** 内部路由 path;解析不出时为空串 */
|
||||||
|
path: string;
|
||||||
|
/** 外部链接(完整 URL 且不属于本应用路由),需整页跳转而非 router.push */
|
||||||
|
external: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空配置的统一返回值 */
|
||||||
|
export const EMPTY_MENU_PAGE: ResolvedMenuPage = { path: "", external: false };
|
||||||
|
|
||||||
|
export function resolveMenuPage(
|
||||||
|
page?: string | null,
|
||||||
|
modId?: number | null,
|
||||||
|
name?: string | null
|
||||||
|
): ResolvedMenuPage {
|
||||||
|
const raw = String(page ?? "").trim();
|
||||||
|
const byName = (): string | null => matchByName(name);
|
||||||
|
const byModId = (): string | null => (modId != null ? ROUTE_BY_MODID.get(modId) ?? null : null);
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
const candidate = extractCandidate(raw);
|
||||||
|
const byPath = matchRoute(candidate);
|
||||||
|
if (byPath) return { path: byPath, external: false };
|
||||||
|
|
||||||
|
// 完整 URL:指向本应用入口的不算有效配置(继续用名字/modId 识别),
|
||||||
|
// 其余按外部链接整页打开
|
||||||
|
if (EXTERNAL_RE.test(raw) && !raw.includes("#")) {
|
||||||
|
if (!isAppEntryCandidate(candidate)) {
|
||||||
|
return { path: raw, external: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = byName() ?? byModId();
|
||||||
|
if (fallback) return { path: fallback, external: false };
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[menuRoute] 模块路径/名称/modId 均无法匹配前端路由,请检查基础平台模块配置:",
|
||||||
|
raw,
|
||||||
|
"modId =", modId,
|
||||||
|
"name =", name
|
||||||
|
);
|
||||||
|
return EMPTY_MENU_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未配置 path:按模块名 → modId 契约解析
|
||||||
|
const fallback = byName() ?? byModId();
|
||||||
|
if (fallback) return { path: fallback, external: false };
|
||||||
|
console.warn("[menuRoute] 模块未配置路径且名称/modId 无法识别:", name, "modId =", modId);
|
||||||
|
return EMPTY_MENU_PAGE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { useRequest } from "alova/client";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { homeworkApi } from "@/api/module/homework";
|
||||||
|
import type { HomeworkVo, ApprovalQuery } from "@/types/homework";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import Utils from "@/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批列表:展示「需审批后发布」的作业(approvalEnabled=1)。
|
||||||
|
* 状态过滤:pending(缺省) / approved / rejected / all;
|
||||||
|
* 操作:查看作业(详情)→ 通过 / 驳回(填审批意见)。
|
||||||
|
*/
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const keyword = ref("");
|
||||||
|
const status = ref("pending");
|
||||||
|
const pageNum = ref(1);
|
||||||
|
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
|
||||||
|
|
||||||
|
const { data: page, loading, send: reload } = useRequest(
|
||||||
|
(params: ApprovalQuery) => homeworkApi.approvalList(params),
|
||||||
|
{ immediate: false, initialData: { content: [], total: 0, pageIndex: 1, pageSize: 10, totalPages: 0, hasNext: false } }
|
||||||
|
);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
await reload({ keyword: keyword.value || "", status: status.value, pageIndex: pageNum.value, pageSize: pageSize.value });
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取审批列表失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function doSearch() {
|
||||||
|
pageNum.value = 1;
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
function pageUpdate(p: number) {
|
||||||
|
pageNum.value = p;
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
function pageSizeUpdate(size: number) {
|
||||||
|
pageNum.value = 1;
|
||||||
|
pageSize.value = size;
|
||||||
|
localStorage.setItem("customPageCount", String(size));
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(run);
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => run()
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------- 审批操作 ---------- */
|
||||||
|
function view(row: HomeworkVo) {
|
||||||
|
router.push({ path: "/homeworklist/detail", query: { serid: row.id, type: 0 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function approve(row: HomeworkVo) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "审批通过",
|
||||||
|
msg: `确定通过「 ${row.title} 」的当前审批步骤吗?`,
|
||||||
|
info: "会签需全部通过、或签任一人通过即进入下一步;最后一步通过后发布",
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
const res: any = await homeworkApi.approve((row as any).id).send();
|
||||||
|
Message.success(res?.status === "approved" ? "已通过全部步骤,作业已发布" : "本步骤已通过,流转至下一步");
|
||||||
|
run();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 驳回:弹窗填写审批意见 */
|
||||||
|
const rejectModal = ref(false);
|
||||||
|
const rejectRow = ref<HomeworkVo | null>(null);
|
||||||
|
const rejectRemark = ref("");
|
||||||
|
function openReject(row: HomeworkVo) {
|
||||||
|
rejectRow.value = row;
|
||||||
|
rejectRemark.value = "";
|
||||||
|
rejectModal.value = true;
|
||||||
|
}
|
||||||
|
async function doReject() {
|
||||||
|
if (!rejectRemark.value.trim()) {
|
||||||
|
Message.info("请填写驳回意见");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await homeworkApi.reject(rejectRow.value!.id, rejectRemark.value.trim()).send();
|
||||||
|
Message.success("已驳回,退回发布人");
|
||||||
|
rejectModal.value = false;
|
||||||
|
run();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 状态标签配色 ---------- */
|
||||||
|
const STATUS_TAG: Record<string, string> = { pending: "warning", approved: "success", rejected: "error", none: "default" };
|
||||||
|
const approvalTag = (row: any) => STATUS_TAG[row.approvalStatus] || "default";
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||||
|
{ title: "标题", key: "title", align: "left", tooltip: true },
|
||||||
|
{ title: "来源部门", key: "deptName", width: 130, align: "left" },
|
||||||
|
{ title: "发布人", key: "publisherName", width: 110, align: "left", tooltip: true },
|
||||||
|
{
|
||||||
|
title: "提交时间",
|
||||||
|
width: 170,
|
||||||
|
align: "left",
|
||||||
|
render: (h: any, p: any) => h("span", {}, String(p.row.createTime || "").slice(0, 16)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "审批状态",
|
||||||
|
width: 240,
|
||||||
|
align: "left",
|
||||||
|
render: (h: any, p: any) => h("Tag", { color: approvalTag(p.row) }, () => p.row.approvalStatusDesc || "—"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "审批意见",
|
||||||
|
key: "approvalRemark",
|
||||||
|
align: "left",
|
||||||
|
width: 180,
|
||||||
|
tooltip: true,
|
||||||
|
render: (h: any, p: any) => h("span", {}, p.row.approvalRemark || "—"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
width: 240,
|
||||||
|
align: "center",
|
||||||
|
fixed: "right",
|
||||||
|
render: (h: any, p: any) => {
|
||||||
|
const row: any = p.row;
|
||||||
|
const ops = [Utils.operatingHandle(h, "查看作业", "icon-browse", () => view(row))];
|
||||||
|
if (row.canApprove) {
|
||||||
|
ops.push(Utils.operatingHandle(h, "通过", "icon-ckpy", () => approve(row)));
|
||||||
|
ops.push(Utils.operatingHandle(h, "驳回", "icon-pingbi", () => openReject(row)));
|
||||||
|
} else if (row.approvalStatus === "pending") {
|
||||||
|
// 非当前审批步骤的审批人:仅展示,不出现操作按钮
|
||||||
|
ops.push(h("span", { style: "color:#999;font-size:12px;margin-left:8px" }, "等待他人审批"));
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="main-container">
|
||||||
|
<!-- 搜索区 -->
|
||||||
|
<div class="search-wrap fix">
|
||||||
|
<RadioGroup v-model="status" type="button" button-style="solid" class="fl" @on-change="doSearch">
|
||||||
|
<Radio label="pending"><span>待审批</span></Radio>
|
||||||
|
<Radio label="approved"><span>已通过</span></Radio>
|
||||||
|
<Radio label="rejected"><span>已驳回</span></Radio>
|
||||||
|
<Radio label="all"><span>全部</span></Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<Input
|
||||||
|
class="fr"
|
||||||
|
style="width: 220px"
|
||||||
|
search
|
||||||
|
suffix="ios-search"
|
||||||
|
placeholder="输入标题/发布人搜索"
|
||||||
|
v-model="keyword"
|
||||||
|
@on-search="doSearch"
|
||||||
|
@on-clear="doSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 驳回弹窗 -->
|
||||||
|
<Modal v-model="rejectModal" title="驳回作业" :mask-closable="false" width="460px">
|
||||||
|
<Form :label-width="80">
|
||||||
|
<FormItem label="作业标题">
|
||||||
|
<span>{{ rejectRow?.title }}</span>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="审批意见">
|
||||||
|
<Input v-model="rejectRemark" type="textarea" :rows="3" placeholder="请填写驳回原因,将退回发布人"></Input>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button type="error" @click="doReject">确认驳回</Button>
|
||||||
|
<Button @click="rejectModal = false">取消</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
|
import { useRequest } from "alova/client";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import { approvalSettingApi, homeworkApi } from "@/api/module/homework";
|
||||||
|
import type { ApproverVo } from "@/types/homework";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import Utils from "@/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核设置:维护「审批人候选池」。发布时从这里选审批人——选到人即走审批流程。
|
||||||
|
* 逻辑很简单:任一审批人通过即发布,驳回退回发布人;也可在发布界面配多步会签/或签。
|
||||||
|
*/
|
||||||
|
const { data: deptOptions } = useRequest(homeworkApi.getSysDic());
|
||||||
|
const approvers = ref<ApproverVo[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function loadApprovers() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
approvers.value = (await approvalSettingApi.approvers().send()) || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "获取审批人失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = ref(false);
|
||||||
|
const editingId = ref<number | null>(null);
|
||||||
|
const form = reactive({ account: "", name: "", deptId: "" });
|
||||||
|
const formRef = ref();
|
||||||
|
const rule = {
|
||||||
|
name: [{ required: true, message: "请输入审批人姓名", trigger: "blur" }],
|
||||||
|
deptId: [{ required: true, message: "请选择所属部门", trigger: "change" }],
|
||||||
|
};
|
||||||
|
function addApprover() {
|
||||||
|
editingId.value = null;
|
||||||
|
form.account = "";
|
||||||
|
form.name = "";
|
||||||
|
form.deptId = "";
|
||||||
|
modal.value = true;
|
||||||
|
}
|
||||||
|
function editApprover(row: ApproverVo) {
|
||||||
|
editingId.value = row.id;
|
||||||
|
form.account = row.account;
|
||||||
|
form.name = row.name;
|
||||||
|
form.deptId = row.deptId;
|
||||||
|
modal.value = true;
|
||||||
|
}
|
||||||
|
async function saveApprover() {
|
||||||
|
const valid = await formRef.value?.validate();
|
||||||
|
if (!valid) {
|
||||||
|
Message.info("请填写红色星号内容!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editingId.value == null) {
|
||||||
|
await approvalSettingApi.addApprover({ account: form.account.trim(), name: form.name.trim(), deptId: form.deptId }).send();
|
||||||
|
} else {
|
||||||
|
await approvalSettingApi.updateApprover({ id: editingId.value, name: form.name.trim(), deptId: form.deptId }).send();
|
||||||
|
}
|
||||||
|
Message.success("操作成功!");
|
||||||
|
modal.value = false;
|
||||||
|
await loadApprovers();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function toggleApprover(row: ApproverVo) {
|
||||||
|
const enabled = row.enabled === 1 ? 0 : 1;
|
||||||
|
try {
|
||||||
|
await approvalSettingApi.updateApprover({ id: row.id, enabled }).send();
|
||||||
|
row.enabled = enabled;
|
||||||
|
Message.success("操作成功!");
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function removeApprover(row: ApproverVo) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "删除",
|
||||||
|
msg: `确定删除审批人「 ${row.name} 」吗?`,
|
||||||
|
info: "删除后不可恢复",
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
await approvalSettingApi.delApprover(row.id).send();
|
||||||
|
Message.success("删除成功");
|
||||||
|
await loadApprovers();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e.message || "删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: "姓名", key: "name", align: "left", minWidth: 120 },
|
||||||
|
{ title: "账号", key: "account", align: "left", width: 130 },
|
||||||
|
{ title: "所属部门", key: "deptName", align: "left", minWidth: 130 },
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => h("Tag", { color: p.row.enabled === 1 ? "success" : "default" }, () => (p.row.enabled === 1 ? "启用" : "停用")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
width: 220,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => {
|
||||||
|
const row: ApproverVo = p.row;
|
||||||
|
return [
|
||||||
|
Utils.operatingHandle(h, row.enabled === 1 ? "停用" : "启用", "icon-pingbi", () => toggleApprover(row)),
|
||||||
|
Utils.operatingHandle(h, "编辑", "icon-edit", () => editApprover(row)),
|
||||||
|
Utils.operatingHandle(h, "删除", "icon-ashbin", () => removeApprover(row)),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: false }));
|
||||||
|
onMounted(loadApprovers);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="main-container">
|
||||||
|
<Alert show-icon type="info">
|
||||||
|
这里维护可被选为审批人的名单。<b>发布或编辑作业时,只要选中了审批人,作业就会进入审批流程</b>(默认一步·或签,也可配多步会签/或签);任一审批人通过即发布,驳回退回发布人。
|
||||||
|
</Alert>
|
||||||
|
<div class="search-wrap fix">
|
||||||
|
<Button class="fl" type="primary" @click="addApprover">新增审批人</Button>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :data="approvers" :loading="loading" class="link-table table-bottom-box" :height="tableHeight" no-data-text="暂无审批人"></Table>
|
||||||
|
|
||||||
|
<Modal v-model="modal" :mask-closable="false" width="450px" :title="editingId == null ? '新增审批人' : '编辑审批人'">
|
||||||
|
<Form ref="formRef" :model="form" :rules="rule" :label-width="90">
|
||||||
|
<FormItem label="姓名:" prop="name">
|
||||||
|
<Input v-model="form.name" placeholder="请输入审批人姓名" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="账号:" prop="account">
|
||||||
|
<Input v-model="form.account" placeholder="登录账号(可选)" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="所属部门:" prop="deptId">
|
||||||
|
<Select v-model="form.deptId" placeholder="请选择部门" transfer>
|
||||||
|
<Option v-for="d in deptOptions" :key="d.k" :value="d.k">{{ d.v }}</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button type="primary" @click="saveApprover">提交</Button>
|
||||||
|
<Button @click="modal = false">取消</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.*
|
||||||
|
# (.env.base/.env.ld/.env.prod 已删,线上由 /api/env-config?target=front 运行时下发)。
|
||||||
|
# ⚠️ VITE_* 仅本地开发生效。
|
||||||
|
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8101),
|
||||||
|
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
|
||||||
|
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||||
|
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||||
|
VITE_SERVICE_HOST=http://base.yunhaixingtong.com/campushomework/
|
||||||
|
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||||
|
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
||||||
|
VITE_IS_MOCK=false
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 本地纯前端调试模式:仅 vite dev --mode mock 加载(build 产物不读任何 .env.*,
|
||||||
|
# 线上由 /api/env-config?target=front 运行时下发)。后端 SSO/base 不可达时启用内置 mock
|
||||||
|
# 启动:npm run dev:mock
|
||||||
|
VITE_BASE_HOST=
|
||||||
|
VITE_SSO_HOST=
|
||||||
|
VITE_SERVICE_HOST=http://localhost:8101
|
||||||
|
VITE_ZIYUANKU_HOST=
|
||||||
|
VITE_ZHUANXIANG_HOST=
|
||||||
|
VITE_IS_MOCK=true
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
yarn.lock
|
||||||
|
.npmrc
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# scratch
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# 改动前本地备份(版本迭代时人工留存,不进版本库)
|
||||||
|
.backup/
|
||||||
|
|
||||||
|
# 备份 / 临时文件(旧版图标集 icons.css.bak 等不入库)
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["airbnb", "prettier", "prettier/react"],
|
||||||
|
singleQuote: false, // 是否强制使用单引号
|
||||||
|
printWidth: 120, // 超过最大值换行
|
||||||
|
tabWidth: 4,
|
||||||
|
htmlWhitespaceSensitivity: "ignore",
|
||||||
|
semi: true, // 结尾是否使用分号
|
||||||
|
//disableLanguages: ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
|
||||||
|
endOfLine: "auto",
|
||||||
|
arrowParens: "avoid", // 箭头函数参数括号 默认avoid 可选 avoid| always
|
||||||
|
};
|
||||||
|
//如需显示webpack目录,运行以下命令
|
||||||
|
// npm run eject
|
||||||
|
//如需配合使eslint生效,运行以下命令
|
||||||
|
//npm install --save-dev eslint-plugin-prettier
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
@@ -1,3 +1,56 @@
|
|||||||
# coursehomework-front
|
# 课程作业前端(Vue3 + Vite + view-ui-plus)
|
||||||
|
|
||||||
managed by projman
|
## 本地启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1)纯前端调试(推荐,后端不可用时)
|
||||||
|
|
||||||
|
本地连不上 `sso.yunhaixingtong.com` / `base.yunhaixingtong.com` 时,后端 CAS 过滤器会拦截
|
||||||
|
`/api/*` 请求导致所有接口失败。此时使用内置 mock 调试前端页面:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:mock
|
||||||
|
```
|
||||||
|
|
||||||
|
- 启用 `src/assets/mock/homeworkapi.ts` 中的内存 mock(vite-plugin-mock),覆盖全部页面接口:
|
||||||
|
用户/菜单、发文部门字典、作业增删改查、阅读情况、联系组、选择用户树、附件上传等。
|
||||||
|
- mock 数据为内存态,刷新页面即还原;修改 mock 文件后需重启 dev server 才生效。
|
||||||
|
- mock 模式下请求走 vite 同源(`host.config.ts` 中 `serviceHost` 为空串)。
|
||||||
|
|
||||||
|
### 2)连接后端调试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.env.dev`:`VITE_SERVICE_HOST=http://localhost:8070/homework`(Java 后端,见 `../service`)。
|
||||||
|
- 后端需要能访问 CAS(`yh.sso.server-url-prefix`)与用户中心(`yh.open.*`),否则接口 302/失败。
|
||||||
|
|
||||||
|
## 主要页面
|
||||||
|
|
||||||
|
| 路由 | 页面 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/homework/newhomework` | 作业发送(新建/编辑 `?serid=`/转发 `?fid=`,wangeditor 富文本) |
|
||||||
|
| `/homework/homework_list` | 已发作业 |
|
||||||
|
| `/homework/homework_accept` | 已收作业(左列表 + 右详情,打印/转发/附件) |
|
||||||
|
| `/homework/manage` | 作业列表(管理端) |
|
||||||
|
| `/homework/setting` | 应用设置(发文部门卡片 + 自定义组) |
|
||||||
|
| `/homework/homework_details` | 作业详情(`type=0` 管理视角含阅读进度/编辑/打印,`type=1` 查看) |
|
||||||
|
|
||||||
|
## 关键组件
|
||||||
|
|
||||||
|
- `components/UserCheckModal.vue`:选择用户弹窗(组织树勾选 + 已选列表,移植自旧前端
|
||||||
|
`oldfront/src/components/userCheck.vue`)。
|
||||||
|
- `components/UserCheckInput.vue`:作业对象选择(全部用户/指定用户,移植自旧
|
||||||
|
`user_check_input.vue`)。
|
||||||
|
- `components/HomeworkUpload.vue`:作业附件上传(选择文件 + 200MB 限制,移植自旧
|
||||||
|
`uploadFile.vue`)。
|
||||||
|
- 发文部门卡片:见 `views/homework/HomeworkSetting.vue`,样式与交互对齐 UserBase「应用类型」
|
||||||
|
(`UserBase/front/src/views/app/AppCategory.vue`),额外支持停用/启用。
|
||||||
|
|
||||||
|
## 统计说明
|
||||||
|
|
||||||
|
统计相关前端已按需求移除(`Homework_statis` 接口封装、菜单、图标等),后期统计不在本应用内做。
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { GeneratorConfig } from "@alova/wormhole";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generator: [
|
||||||
|
{
|
||||||
|
input: "http://base.yunhaixingtong.com/homework/v3/api-docs",
|
||||||
|
// 文档平台类型
|
||||||
|
platform: "swagger",
|
||||||
|
|
||||||
|
// 生成代码的输出目录
|
||||||
|
output: "src/api",
|
||||||
|
// 响应数据的媒体类型
|
||||||
|
responseMediaType: "application/json",
|
||||||
|
// 请求体数据的媒体类型
|
||||||
|
bodyMediaType: "application/json",
|
||||||
|
// 生成的 API 版本
|
||||||
|
version: 1,
|
||||||
|
// 生成代码类型
|
||||||
|
type: "typescript",
|
||||||
|
// 全局 API 名称
|
||||||
|
global: "Apis",
|
||||||
|
splitImplementation: true,
|
||||||
|
implementation: "inline",
|
||||||
|
// API 处理函数
|
||||||
|
handleApi: apiDescriptor => {
|
||||||
|
// 过滤掉已废弃的 API
|
||||||
|
if (apiDescriptor.deprecated) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const newOpId = apiDescriptor.summary?.trim() || apiDescriptor.operationId;
|
||||||
|
const url = apiDescriptor.url.replace(/^\/{platform}\//, "/api/");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...apiDescriptor,
|
||||||
|
operationId: newOpId, // 关键:改掉它
|
||||||
|
url: apiDescriptor.url.replace(/^\/{platform}\//, "/api/"),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as GeneratorConfig;
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
# 课程作业模块 · 前后端 Bug 检测与修复计划任务
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| 前端 | `E:\YHXT\view\CourseHomework`(Vue3 + Vite + TS + view-ui-plus + alova) |
|
||||||
|
| 后端 | `E:\YHXT\java\ClassHomeWork`(Spring Boot 2.7 + JPA,`/{platform}/…` 统一前缀,`platform=api`) |
|
||||||
|
| 检测日期 | 2026-09-16 |
|
||||||
|
| 检测方式 | 静态检视 + 契约逐字段对照 + 构建/类型检查 |
|
||||||
|
| 文档性质 | 计划任务文档(检测项清单)+ 执行记录(修复与回归结果) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 检测范围与方法
|
||||||
|
|
||||||
|
### 0.1 范围
|
||||||
|
|
||||||
|
| 层 | 覆盖对象 |
|
||||||
|
|---|---|
|
||||||
|
| 前端契约层 | `src/types/homework.ts`、`src/types/homeworkV2.ts` |
|
||||||
|
| 前端 API 层 | `src/api/module/{homework,teacherHomework,studentHomework,notification}.ts`、`src/utils/alova/*` |
|
||||||
|
| 前端页面 | `src/views/homework/*.vue`(9 个页面) |
|
||||||
|
| 前端组件/路由 | `src/components/{ReviewWorkbench,ReviewModal,AttachmentUpload,layout}.vue`、`src/router/routes.ts`、`src/utils/menuRoute.ts`、`src/App.vue` |
|
||||||
|
| Mock | `src/assets/mock/homeworkapi.ts` |
|
||||||
|
| 后端接口面 | `controller/api/{TeacherHomeworkV2,StudentHomework,Notification,Homework,File,BaseApi}Controller` |
|
||||||
|
| 后端模型 | `model/homework/*`、`model/v2/*`、`model/BasePage*` |
|
||||||
|
| 后端服务 | `service/impl/{TeacherHomeworkV2,StudentHomework,Homework,HomeworkProgressCalculator}Service` |
|
||||||
|
|
||||||
|
### 0.2 方法
|
||||||
|
|
||||||
|
1. **静态检测**:`vite build`(生产构建)、`vue-tsc --noEmit`(类型检查)。
|
||||||
|
2. **契约逐字段对照**:前端 `types/*.ts` ↔ 后端 `model/**/*.java` 字段名与类型逐一对照。
|
||||||
|
3. **端点对照**:前端 API 客户端方法与后端 Controller `@RequestMapping/@GetMapping/@PostMapping` 逐一对照(路径、动词、出入参)。
|
||||||
|
4. **引用完整性**:已删除组件残留引用、未使用组件、未定义全局符号。
|
||||||
|
5. **运行期口径**:状态机(`hwStatus` 五态 / `submitStatus` 五态 / `reviewStatus` 三态)在前后端的判定一致性。
|
||||||
|
|
||||||
|
### 0.3 检测项清单(共 24 项)
|
||||||
|
|
||||||
|
| 编号 | 检测项 | 方法 |
|
||||||
|
|---|---|---|
|
||||||
|
| C-01 | 生产构建是否通过 | `vite build --mode base` |
|
||||||
|
| C-02 | TS 类型检查是否通过 | `vue-tsc --noEmit` |
|
||||||
|
| C-03 | 已删除组件是否仍被引用 | grep 残留引用 |
|
||||||
|
| C-04 | 是否存在未使用的死组件 | 反向引用计数 |
|
||||||
|
| C-05 | 前端定义的接口路径后端是否真实存在 | 端点对照 |
|
||||||
|
| C-06 | 路径参数名是否与后端 `@PathVariable` 一致 | 端点对照 |
|
||||||
|
| C-07 | `@RequestBody` 是否用 POST/PUT + JSON | 端点对照 |
|
||||||
|
| C-08 | 分页外壳字段是否一致 | `BasePageResult` 对照 |
|
||||||
|
| C-09 | 分页入参语义是否一致(pageIndex 起始值) | `BasePageQuery` 对照 |
|
||||||
|
| C-10 | V2 作业 VO 字段是否一致 | `HomeworkV2Vo` 对照 |
|
||||||
|
| C-11 | V2 学生 VO 字段是否一致 | `StudentHomeworkVo/DetailVo` 对照 |
|
||||||
|
| C-12 | 学生矩阵 VO 字段是否一致 | `StudentMatrixVo` 对照 |
|
||||||
|
| C-13 | 提交版本 VO 字段是否一致 | `SubmissionVersionVo` 对照 |
|
||||||
|
| C-14 | 通知 VO / 未读数是否一致 | `NotificationPageResult` 对照 |
|
||||||
|
| C-15 | V1 作业列表 VO 字段是否一致 | `HomeworkVo` 对照 |
|
||||||
|
| C-16 | V1 详情 VO 字段是否一致 | `HomeworkDetailVo` 对照 |
|
||||||
|
| C-17 | V1 统计 VO 字段是否一致 | `HomeworkStatisticsVo` 对照 |
|
||||||
|
| C-18 | 时间格式是否一致(全局 `yyyy-MM-dd HH:mm:ss`) | `JacksonConfig` + 前端解析 |
|
||||||
|
| C-19 | 附件 URL 是否可解析(相对/绝对、contextPath) | 各页面 `previewUrl/downUrl` |
|
||||||
|
| C-20 | 状态机判定是否前后端一致(可提交/可撤回/可编辑) | 页面 `canSubmit*` vs Service 校验 |
|
||||||
|
| C-21 | 是否存在重复函数声明/无界缓存 | 代码检视 |
|
||||||
|
| C-22 | 组件挂载期是否存在异步竞态 | 代码检视 |
|
||||||
|
| C-23 | 构建/发布配置是否自洽(outDir、env、产物) | `vite.config.ts` + `.env.*` |
|
||||||
|
| C-24 | 工作区未提交改动是否引入噪音 | `git status/diff` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 检测结论摘要
|
||||||
|
|
||||||
|
| 编号 | 问题 | 级别 | 影响面 | 状态 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| B-01 | 学生端缺少 `allowLate` 字段,导致「截止后补交」能力在真实环境完全失效 | **P0** | 学生列表/详情/快捷提交 | 已修复 |
|
||||||
|
| B-02 | `ReviewWorkbench` 附件 URL 未拼 `serviceHost`,线上图片/下载 404 | **P0** | 教师批阅工作台 | 已修复 |
|
||||||
|
| B-03 | `HomeworkStatisticsVo` 前端声明 5 个幻影字段,后端从未返回 → 统计页 4 张卡片恒为 0/0%/空表 | **P0** | `/stats` 作业统计 | 已修复(前端降级)+ 后端补字段 |
|
||||||
|
| B-04 | `HomeworkVo` 前端声明 `progress` 字段,后端从未返回 → 「全部作业」页 3 列进度恒为「—」 | **P0** | `/homeworklist` 全部作业 | 已修复(前端降级)+ 后端补字段 |
|
||||||
|
| B-05 | `HomeworkDetailVo` 前端声明 `receiveText`,后端从未返回 → 详情页「接收班级」恒显示「全部班级」 | **P1** | `/homeworklist/detail` | 已修复(后端补字段 + 前端兜底) |
|
||||||
|
| B-06 | `TeacherHomeworks.vue` 4 个行操作函数重复声明 | **P1** | `/list` 我布置的 | 已修复 |
|
||||||
|
| B-07 | `TeacherHomeworks.vue` `moreActions` Map 每次重渲染累积、永不回收 | **P1** | `/list` 我布置的 | 已修复 |
|
||||||
|
| B-08 | `NewHomework.vue` 编辑回显与班级树加载存在竞态,导致「全部班级」勾选态误判 | **P1** | `/list/new` 发作业 | 已修复 |
|
||||||
|
| B-09 | `src/api/index.ts` 引用未定义的 `GlobalFetch()`,且 baseURL 硬编码远程域名 | **P2** | 潜在全局 auto-import 污染 | 已修复 |
|
||||||
|
| B-10 | 7 个死组件 + 1 个 `index copy.vue` 残留,`auto-import` 扫描目录存在误引入风险 | **P2** | 构建体积/可维护性 | 已记录(不删除) |
|
||||||
|
| B-11 | `.backup/` 未纳入 `.gitignore`,污染 `git status` | **P2** | 版本管理 | 已修复 |
|
||||||
|
| B-12 | `routes.ts` 出现仅换行符差异的伪改动 | **P2** | 版本管理噪音 | 已记录 |
|
||||||
|
| B-13 | `baseApi.setRole` 指向后端不存在的 `/api/base/SetRole`,真实环境每次跨角色路由切换都会 404 | **P2** | 控制台噪音 | 已记录(设计如此) |
|
||||||
|
| B-14 | `vue-tsc` 在 Node 24 下崩溃(`Search string not found: "/supportedTSExtensions = .*(?=;)/"`),类型门禁实际失效 | **P1** | 全部 TS 代码 | 已记录(需换 Node 18/20 或升级 vue-tsc) |
|
||||||
|
| C-01 | 生产构建通过(39.66s,exit 0) | — | — | 通过 |
|
||||||
|
| C-02 | TS 类型检查**无法执行**(见 B-14) | — | — | 阻塞 |
|
||||||
|
| C-05~C-07 | V2 端点 28 个 + 通知 3 个 + V1 冻结端点路径/动词与后端一致 | — | — | 通过 |
|
||||||
|
| C-08/C-09 | `BasePageResult` 六字段、`pageIndex` 从 1 起前后端一致 | — | — | 通过 |
|
||||||
|
| C-10~C-14 | V2 作业/学生/矩阵/版本/通知 VO 字段与后端逐一对应 | — | — | 通过 |
|
||||||
|
| C-18 | 后端 `JacksonConfig` 全局 `yyyy-MM-dd HH:mm:ss`(无 `T`),前端一律 `String(v).slice()` 截取,一致 | — | — | 通过 |
|
||||||
|
| C-23 | `outDir=dist` 直出、`.env.dev/.env.mock` 仅本地生效、产物不含环境值,自洽 | — | — | 通过 |
|
||||||
|
|
||||||
|
**最严重的三条(P0)本质是同一个病根**:前端契约(`types/*.ts`)比后端实际返回**多声明的字段**,被 TypeScript 视为可选(`?:`),又被 `vue-tsc` 崩溃掩盖,于是「编译通过、构建成功、mock 模式下一切正常」——**只有连真实后端才会表现为静默的空值/功能失效**。mock(`homeworkapi.ts`)为了让页面「跑得起来」,主动补齐了这些后端并不存在的字段,进一步掩盖了问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 缺陷详情与修复方案
|
||||||
|
|
||||||
|
### B-01 【P0】学生端 `allowLate` 字段缺失 → 截止后补交能力失效
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- 后端 `model/v2/StudentHomeworkVo.java` 字段仅:`id/title/subject/teacherName/deadlineAt/submitType/hwStatus/hwStatusDesc/my`,**无 `allowLate`**。
|
||||||
|
- 后端实体 `entity/Homework.java:188` 确有 `allow_late` 列(`private Integer allowLate = 0`),说明数据存在、只是学生端 VO 未透出。
|
||||||
|
- 后端 `service/impl/StudentHomeworkService.java#copyBase()`(411~433 行)逐字段赋值,**未赋值 allowLate**。
|
||||||
|
- 前端 `views/homework/StudentHomeworks.vue:110`:`if (x.hwStatus === "CLOSED") return x.allowLate === 1;`
|
||||||
|
- 前端 `views/homework/HomeworkAccept.vue:104`:`if (d.hwStatus === "CLOSED") return d.allowLate === 1;`;第 307 行提示语 `v-if="detail.allowLate !== 1"`。
|
||||||
|
- 前端类型 `types/homeworkV2.ts` 的 `StudentHomeworkV2Vo` **也未声明 `allowLate`**(TS 应报错,被 B-14 掩盖)。
|
||||||
|
- mock 学生列表/详情响应同样未输出 `allowLate`。
|
||||||
|
|
||||||
|
**后果**
|
||||||
|
|
||||||
|
`x.allowLate` 恒为 `undefined` → `undefined === 1` 恒 `false`。因此后端即便 `allow_late=1`(`doSubmit` 允许 `LATE_SUBMITTED`),前端也**永远不渲染「补交作业」按钮**,并恒显示「作业已截止,如需补交请联系老师」。这是真实环境下可直接复现的功能性缺陷——接口允许、UI 拦住。
|
||||||
|
|
||||||
|
**修复**
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|---|---|
|
||||||
|
| `ClassHomeWork/model/v2/StudentHomeworkVo.java` | 新增 `private Integer allowLate;`(含注释) |
|
||||||
|
| `ClassHomeWork/service/impl/StudentHomeworkService.java` | `copyBase()` 增加 `vo.setAllowLate(h.getAllowLate());` |
|
||||||
|
| 前端 `types/homeworkV2.ts` | `StudentHomeworkV2Vo` 新增 `allowLate?: number;` |
|
||||||
|
| 前端 `assets/mock/homeworkapi.ts` | 学生列表与详情响应补 `allowLate: n.allowLate ?? 0` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-02 【P0】`ReviewWorkbench` 附件 URL 未拼 `serviceHost` → 线上 404
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- `components/AttachmentUpload.vue` 上传成功后写入 `fileUrl: "/api/file/Preview?id=" + fileid`(**相对路径**)。
|
||||||
|
- `views/homework/StudentHomeworks.vue`、`HomeworkAccept.vue`、`Submissions.vue` 展示时均用 `serviceBase`(`config.serviceHost`)拼接相对路径。
|
||||||
|
- 但 `components/ReviewWorkbench.vue`(教师批阅工作台,被 `Grade.vue` 与 `ReviewModal.vue` 共用,即 `/grade` 与 `/submissions` 两个入口):
|
||||||
|
```ts
|
||||||
|
function attUrl(a: AttachmentItem): string {
|
||||||
|
if (/^(https?:)?\/\//.test(a.fileUrl) || a.fileUrl.startsWith("data:")) return a.fileUrl;
|
||||||
|
return a.fileUrl; // ← 相对路径原样返回,未拼 serviceBase
|
||||||
|
}
|
||||||
|
function attDownUrl(a: AttachmentItem): string {
|
||||||
|
return a.fileId ? `/api/file/Down?id=${a.fileId}` : a.fileUrl; // ← 同样缺前缀
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**后果**
|
||||||
|
|
||||||
|
本地 `serviceHost` 为空(走 vite 同源代理)时正常;线上 `serviceHost` 为基础平台域名且应用部署于 `/{contextPath}/front/`,相对路径 `/api/...` 会被解析到**站点根**而非后端 contextPath,导致批阅时学生作答图片**裂图**、附件**下载 404**。教师端核心链路(看图给分)受阻。
|
||||||
|
|
||||||
|
**修复**:`ReviewWorkbench.vue` 引入 `@/assets/config/host.config`,按 `config.isMock ? "" : config.serviceHost` 前缀拼接(与其它页面同一范式)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-03 【P0】统计页幻影字段 → 4 张卡片恒为 0
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- 前端 `types/homework.ts` 的 `HomeworkStatisticsVo` 声明:`myAssignCount?`、`pendingGrade?`、`avgSubmitRate?`、`byHomework?`、`scoreDist?`。
|
||||||
|
- 后端 `model/homework/HomeworkStatisticsVo.java` 实际字段仅:`total`、`publishedCount`、`totalReadRecords`、`unreadForCurrentUser`、`byPublisher`、`byMonth`。
|
||||||
|
- 后端 `service/impl/HomeworkService.java#statistics()`(702~756 行)只 set 这 6 个字段,**全库 grep `myAssignCount|avgSubmitRate|scoreDist|byHomework` 在 `src/main/java` 下零命中**。
|
||||||
|
- `views/homework/HomeworkStats.vue` 直接消费:4 张卡片 `myAssignCount/pendingGrade/avgSubmitRate/total`、成绩分布 `stat.scoreDist`、各作业进度表 `stat.byHomework`。
|
||||||
|
- mock `/api/homework/Statis`(`homeworkapi.ts:1319`)**主动补齐了**这 5 个字段 → mock 模式下页面完整,真实后端下全空。
|
||||||
|
|
||||||
|
**后果**:真实环境 `/stats` 页显示「我布置的作业 0 / 待批改总量 0 / 平均提交率 0%」、成绩分布条全 0、各作业进度表「暂无数据」。属静默错误,用户会误以为没有数据。
|
||||||
|
|
||||||
|
**修复(双管齐下)**
|
||||||
|
|
||||||
|
1. **前端降级(可验证)**:`HomeworkStats.vue` 在字段缺失时明确呈现「后端未提供该口径」而非伪装成 0;仅在 `scoreDist?.length` / `byHomework?.length` 时渲染区块。
|
||||||
|
2. **后端补字段(additive)**:`HomeworkStatisticsVo` 新增这 5 个字段,`statistics()` 基于 V2 `HomeworkProgressCalculator` 聚合填充。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-04 【P0】「全部作业」页进度列恒空
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- 前端 `types/homework.ts` 的 `HomeworkVo` 声明 `progress?: HomeworkProgress | null`,`HomeworkList.vue` 用 `progressCell / gradeCell` 渲染「提交进度 / 未读未交 / 批改」三列,并读 `g.read`、`g.submitRate`、`g.pendingGrade`、`g.unread`。
|
||||||
|
- 后端 `model/homework/HomeworkVo.java` **没有 `progress` 字段**;`service/impl/HomeworkService.java#toVo()`(903~920 行)只回填 `deadline/submitType/hwStatus/hwStatusDesc/read/readCount/deptName`。
|
||||||
|
- `#adminList()`(345~362 行,即 `/homework/api/homework/SendList`)只额外填充 `UpFile` 附件串,**未填 progress**。
|
||||||
|
- `/homeworklist` 路由与菜单 `modId=3` 均在生效(`routes.ts`、`config/menu.ts`)。
|
||||||
|
|
||||||
|
**后果**:真实环境下「全部作业」页的提交进度条、未读未交、批改状态三列恒为「—」。
|
||||||
|
|
||||||
|
**修复**
|
||||||
|
|
||||||
|
1. **后端补字段**:`HomeworkVo` 新增 `progress`;`adminList()` 用 `HomeworkProgressCalculator` 聚合填充。
|
||||||
|
2. **前端降级**:进度列在 `progress` 缺失时显示「—」(现有行为已如此),不伪装为 0。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-05 【P1】详情页「接收班级」恒显示「全部班级」
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- `views/homework/HomeworkDetails.vue`(V1 管理分支):`<Tag color="blue">接收班级:{{ detail.receiveText || "全部班级" }}</Tag>`。
|
||||||
|
- 后端 `model/homework/HomeworkDetailVo.java` **无 `receiveText`**(字段:`serid/title/Contents/DeptID/DeptName/TeaName/EditDate/numcount/readnumcount/ReceiveList/DsfsTime/isDsfs/ChaoSend/tlist`);全库 `grep receiveText src/main/java` 零命中。
|
||||||
|
- 后端实际提供 `ReceiveList`(值 `"ALL"` 或 id 串)与 `tlist`。
|
||||||
|
|
||||||
|
**修复**:后端 `HomeworkDetailVo` 新增 `receiveText` 并在 `getDetailMap()` 填充可读文本(「全部班级」或班级名串);前端保留 `receiveText || "全部班级"` 兜底。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-06 / B-07 【P1】`TeacherHomeworks.vue` 重复声明与无界缓存
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- `viewDetail / viewSubmissions / toGrade / edit` 四个函数在 `<script setup>` 中**各定义两次**(同一模块作用域重复 `function` 声明)。
|
||||||
|
- `const moreActions = new Map<string, () => void>()`、`let moreSeq = 0` 位于模块级;`pubColumns` 是 `computed`,每次依赖变化重算都会调用 `mItem()` 递增 `moreSeq` 并写入 Map,**Map 只增不减**。
|
||||||
|
|
||||||
|
**后果**:重复声明在 JS 中后者覆盖前者(静默),但违反类型检查(TS `Duplicate function implementation`),也让维护者误判生效分支;Map 在长时间停留/频繁翻页的列表页持续增长(内存缓慢泄漏)。
|
||||||
|
|
||||||
|
**修复**:删除重复的 4 个函数定义;`moreActions` 改为每次 `pubColumns` 重算时重建(局部 Map),避免无界增长。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-08 【P1】`NewHomework.vue` 编辑回显与班级树竞态
|
||||||
|
|
||||||
|
**证据链**
|
||||||
|
|
||||||
|
- `onMounted(loadClasses)` 与 `onMounted(async () => { … await teacherApi.detail(); fillFromV2(d); })` 两次注册,`loadClasses()` 内部 `await teacherApi.classTree()`,**不保证先于详情回显完成**。
|
||||||
|
- `fillFromV2()` 依赖 `allClassIds.value` 判断「是否全部班级」:
|
||||||
|
```ts
|
||||||
|
if (cls.length && cls.length === allClassIds.value.length && cls.every(c => allClassIds.value.includes(c)))
|
||||||
|
allClasses.value = true;
|
||||||
|
```
|
||||||
|
- 若 `allClassIds` 仍为空(`length === 0`),条件恒不成立 → 即使该作业确实覆盖全部班级,也渲染为**未勾选「全部班级」**且多选框中班级列表为空(丢失勾选态)。
|
||||||
|
|
||||||
|
**修复**:把初始化串行为「先 `await loadClasses()`,再拉详情并 `fillFromV2()`」,消除竞态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-09 【P2】`src/api/index.ts` 引用未定义符号
|
||||||
|
|
||||||
|
**证据链**:`src/api/index.ts` 顶部 `createAlova({ … requestAdapter: GlobalFetch() … })`,但该文件**未导入也未定义 `GlobalFetch`**;且 `baseURL: 'http://base.yunhaixingtong.com/homework'` 硬编码远程域名,与 `host.config.ts` 的运行时配置体系冲突。该文件未被任何模块引用,但 `vite.config.ts` 中 `ViteAutoImport({ dirs: ["src/api"] })` 会扫描该目录并生成全局声明(`auto-imports.d.ts` 中可见 `apiDefinitions/createApis/mountApis/withConfigType`),存在被误自动引入、进而在运行时抛 `GlobalFetch is not defined` 的风险。
|
||||||
|
|
||||||
|
**修复**:删除该废弃入口文件(页面统一走 `api/module/*`,`src/api/index.ts` 与 `apiDefinitions.ts`/`createApis.ts` 为 `alova gen` 遗留)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B-10 / B-11 / B-12 / B-13 / B-14 其余记录项
|
||||||
|
|
||||||
|
| 编号 | 内容 | 处置 |
|
||||||
|
|---|---|---|
|
||||||
|
| B-10 | 死组件:`BschImport.vue`、`GroupTreeSearchPanel.vue`、`MyTreeSearch.vue`、`TablePageAlova.vue`、`TablePageAlovaNew.vue`、`UploadFile.vue`、`UploadImg.vue`、`my_tree.vue`、`TreeNode/index copy.vue` 反向引用均为 0 | **不删除**(可能为后续页面预留),仅登记 |
|
||||||
|
| B-11 | `.backup/` 未在 `.gitignore`,`git status` 出现 `?? .backup/20260916-m5-v2-mock/` | 追加忽略规则 |
|
||||||
|
| B-12 | `src/router/routes.ts` 被 `git status` 标记为已修改,但 `git diff` 内容为空(仅 CRLF/LF 差异) | 记录;建议统一 `core.autocrlf` 或提交时按内容判定 |
|
||||||
|
| B-13 | `App.vue` 每次跨角色路由切换调用 `baseApi.setRole` → 后端无 `/api/base/SetRole`,真实环境 404(已被 `try/catch` 吞掉) | 设计如此(仅 mock 用),保留但已在前端注释标明 |
|
||||||
|
| B-14 | `vue-tsc@1.8.27` + Node 24 崩溃,`npm run build` 不含类型检查 → TS 契约门禁失效(正是 B-01/B-03/B-04 长期未被发现的原因) | 需将 Node 降到 18/20 或升级 `vue-tsc`;建议在 CI 增加类型检查步骤 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 执行计划(修复顺序与验证)
|
||||||
|
|
||||||
|
| 步骤 | 动作 | 验证方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| S1 | 修复 B-01(后端 VO + Service;前端类型 + mock) | 后端字段对照;前端构建 |
|
||||||
|
| S2 | 修复 B-02(ReviewWorkbench 拼 `serviceHost`) | 前端构建;代码走查三条附件路径 |
|
||||||
|
| S3 | 修复 B-04 / B-05(后端 `HomeworkVo.progress`、`HomeworkDetailVo.receiveText` + 服务填充) | 字段对照 + 前端构建 |
|
||||||
|
| S4 | 修复 B-03(统计 VO 补字段 + 前端降级展示) | 字段对照 + 前端构建 |
|
||||||
|
| S5 | 修复 B-06 / B-07(`TeacherHomeworks.vue` 去重、Map 本地化) | 前端构建 |
|
||||||
|
| S6 | 修复 B-08(`NewHomework.vue` 串行初始化) | 前端构建 |
|
||||||
|
| S7 | 修复 B-09(删除废弃 `src/api/index.ts`) | 全局 grep 无引用;前端构建 |
|
||||||
|
| S8 | 修复 B-11(`.gitignore` 追加 `.backup/`) | `git status` 干净 |
|
||||||
|
| S9 | 回归:`vite build --mode base` 全绿 | exit code 0 |
|
||||||
|
| S10 | 更新本文档「执行记录」并汇总遗留风险 | 文档 |
|
||||||
|
|
||||||
|
**约束说明**:本机无 Maven、无 JDK(仅 JRE 8),后端 Java 改动**无法本地编译验证**,仅做「纯增量、机械式」修改(新增字段 + 赋值 + 新注入),需在装有 Maven 的环境中执行 `mvn -q clean compile` 复核。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 执行记录
|
||||||
|
|
||||||
|
> 本节由执行过程回填,逐项记录改动文件与验证结果。
|
||||||
|
|
||||||
|
### S1 · B-01 学生端 allowLate 透出
|
||||||
|
|
||||||
|
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/model/v2/StudentHomeworkVo.java`:新增 `allowLate` 字段
|
||||||
|
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/service/impl/StudentHomeworkService.java`:`copyBase()` 补 `vo.setAllowLate(...)`
|
||||||
|
- [x] `src/types/homeworkV2.ts`:`StudentHomeworkV2Vo` 新增 `allowLate?: number`
|
||||||
|
- [x] `src/assets/mock/homeworkapi.ts`:学生列表 + 学生详情响应补 `allowLate`
|
||||||
|
|
||||||
|
### S2 · B-02 批阅工作台附件地址
|
||||||
|
|
||||||
|
- [x] `src/components/ReviewWorkbench.vue`:引入 `host.config`,`attUrl/attDownUrl` 统一补 `serviceHost`
|
||||||
|
|
||||||
|
### S3 · B-04 / B-05 后端 V1 VO 补字段
|
||||||
|
|
||||||
|
- [x] `HomeworkVo.java`:新增 `progress`(`model/v2/ProgressVo`)
|
||||||
|
- [x] `HomeworkDetailVo.java`:新增 `receiveText`
|
||||||
|
- [x] `HomeworkService.java`:`adminList()` 填 `progress`;`getDetailMap()` 填 `receiveText`
|
||||||
|
|
||||||
|
### S4 · B-03 统计口径
|
||||||
|
|
||||||
|
- [x] `HomeworkStatisticsVo.java`:新增 `myAssignCount/pendingGrade/avgSubmitRate/byHomework/scoreDist`
|
||||||
|
- [x] `HomeworkService.java`:`statistics()` 聚合填充
|
||||||
|
- [x] `src/views/homework/HomeworkStats.vue`:字段缺失时降级为「—/暂未提供」,不再伪装为 0
|
||||||
|
|
||||||
|
### S5 · B-06 / B-07 我布置的页
|
||||||
|
|
||||||
|
- [x] `src/views/homework/TeacherHomeworks.vue`:删除 4 个重复函数;`moreActions` 改为列重算时重建
|
||||||
|
|
||||||
|
### S6 · B-08 发作业页竞态
|
||||||
|
|
||||||
|
- [x] `src/views/homework/NewHomework.vue`:初始化改为 `await loadClasses()` → 详情回显串行
|
||||||
|
|
||||||
|
### S7 · B-09 废弃入口
|
||||||
|
|
||||||
|
- [x] 删除 `src/api/index.ts`(含 `GlobalFetch` 未定义、硬编码 baseURL)
|
||||||
|
|
||||||
|
### S8 · B-11 忽略规则
|
||||||
|
|
||||||
|
- [x] `.gitignore` 追加 `.backup/`
|
||||||
|
|
||||||
|
### S9 · 回归验证
|
||||||
|
|
||||||
|
见 §5。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 回归验证结果
|
||||||
|
|
||||||
|
| 验证项 | 命令 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 生产构建 | `npx vite build --mode base` | 见下 |
|
||||||
|
| 端点/契约复核 | 字段对照脚本 | 见下 |
|
||||||
|
| Java 编译 | `mvn clean compile` | **未执行**(本机无 Maven/JDK) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 遗留风险与后续建议
|
||||||
|
|
||||||
|
1. **类型门禁形同虚设(B-14,最高优先)**:本次 B-01/B-03/B-04 三条 P0 全部是「类型声明比后端多」的静默错配。恢复 `vue-tsc` 可运行(Node 18/20 或升级 `vue-tsc`)并纳入 CI,是防止同类问题复发的根本手段。
|
||||||
|
2. **mock 与真实后端口径漂移**:`homeworkapi.ts` 主动补齐了后端不存在的字段,使 mock 模式「看起来一切正常」,掩盖真实缺陷。建议以「后端 VO 为唯一真源」生成/校验 mock。
|
||||||
|
3. **后端改动未经编译验证**:S1/S3/S4 涉及 Java 文件,本机无 Maven/JDK,需在构建环境跑 `mvn -q clean compile` 复核;若编译不通过,回退范围仅限本次新增的字段与注入。
|
||||||
|
4. **`/stats` 与「全部作业」仍建议评估退役**:这两页属 V1 冻结面,其课程作业口径字段(提交进度/批改/成绩分布)在 V2 已有权威实现(`teacherApi.stats` / `published` 的 `progress`)。长期建议整体切 V2,避免在 V1 VO 上持续打补丁。
|
||||||
|
5. **`GitHub` 换行符噪音(B-12)**:建议统一 `core.autocrlf` 或在 `.gitattributes` 中固定 `*.ts text eol=lf`,避免无内容变更的文件反复出现在 diff 中。
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
# 课堂作业前端 M5 切换计划 —— 对齐后端 V2 接口完成页面
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| 项目 | 前端 `E:\YHXT\view\CourseHomework`(Vue3 + Vite + TS + view-ui-plus + alova) |
|
||||||
|
| 依据 | 后端 `E:\YHXT\java\ClassHomeWork` 已实现接口(M1~M4 完成,V3.2 运行验证通过)+ `classwork-homework-plan-v3.md` §11 前端适配计划 / §15.5 遗留待办第 1 条 |
|
||||||
|
| 目标 | 前端 9 个核心页面全部切换到 V2 接口面,打通「发布→统计→批阅→学生提交→结果」全链路联调 |
|
||||||
|
| 后端地址 | 本地 `http://localhost:8102`(vite proxy 已配置,无需改动) |
|
||||||
|
| 文档版本 | V1.0(2026-09-15) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与现状差距
|
||||||
|
|
||||||
|
### 1.1 后端权威接口面(已实现,本计划的对齐目标)
|
||||||
|
|
||||||
|
后端按 PRD §18 完成了 V2 接口(28 个新端点),同时保留 V1 冻结接口(17 个)。统一响应 `{code,msg,data}`,分页 `BasePageResult`,时间格式全局 `yyyy-MM-dd HH:mm:ss`(无 "T" 分隔符)。
|
||||||
|
|
||||||
|
**教师端 `/api/teacher/homeworks`(19 端点,PRD 页 1~6):**
|
||||||
|
|
||||||
|
| 方法 | 路径 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| POST | `` | 创建作业(一律落草稿 DRAFT_UNPUBLISHED) |
|
||||||
|
| PUT | `/{id}` | 编辑作业(仅草稿两态) |
|
||||||
|
| DELETE | `/{id}` | 删除作业(仅草稿两态) |
|
||||||
|
| POST | `/{id}/publish` | 立即发布 |
|
||||||
|
| POST | `/{id}/schedule` | 定时发布 / 取消定时(`{scheduledAt}` 或 `{cancel:true}`) |
|
||||||
|
| GET | `/drafts` | 草稿箱分页(`subject`/`hwStatus`/`keyword` 筛选) |
|
||||||
|
| GET | `/published` | 已发布列表分页(含应交/已读/已交/未读未交聚合) |
|
||||||
|
| POST | `/{id}/remind` | 提醒未读/未交(`{type: NOT_READ\|NOT_SUBMITTED, studentIds?}`,返回 `{reminded}`) |
|
||||||
|
| POST | `/{id}/extend` | 延长截止(`{deadlineAt}` 须晚于当前值) |
|
||||||
|
| POST | `/{id}/copy` | 复制为新草稿,返回新 id |
|
||||||
|
| POST | `/{id}/archive` | 归档(只读,屏蔽提醒) |
|
||||||
|
| GET | `/{id}/export` | 导出学生矩阵 Excel(文件流) |
|
||||||
|
| GET | `/{id}` | 作业详情 |
|
||||||
|
| GET | `/{id}/stats` | 汇总统计卡片(总进度 + 分班进度) |
|
||||||
|
| GET | `/{id}/students` | 学生矩阵分页(`classId`/`submitStatus`/`reviewStatus`/`pendingGrade` 筛选) |
|
||||||
|
| POST | `/{id}/submissions/{studentId}/review` | 保存批阅(`{score?, comment?, markedImages?}`,score/comment 至少一项) |
|
||||||
|
| POST | `/{id}/submissions/{studentId}/return` | 打回(`{comment}` 必填) |
|
||||||
|
| GET | `/{id}/submissions/{studentId}/versions` | 历史提交版本列表 |
|
||||||
|
| GET | `/classes` | 年级→班级树(叶子 id=classId,含 studentCount) |
|
||||||
|
| POST | `/classes/sync` | 手动触发名册同步 |
|
||||||
|
|
||||||
|
**学生端 `/api/student/homeworks`(6 端点,PRD 学生页 1~3):**
|
||||||
|
|
||||||
|
| 方法 | 路径 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `` | 我的作业列表(`tab=all/todo/done/closed` + 分页/keyword) |
|
||||||
|
| GET | `/{id}` | 作业详情(含 description/attachments/myAttachments/myText) |
|
||||||
|
| POST | `/{id}/read` | 阅读上报(前端停留 ≥3 秒后调用,幂等) |
|
||||||
|
| POST | `/{id}/submissions` | 提交作业(`{text?, attachments[]}`),返回 `{submissionId}` |
|
||||||
|
| PUT | `/{id}/submissions/{submissionId}` | 撤回/打回重交(`{action: withdraw\|resubmit, text?, attachments?}`) |
|
||||||
|
| GET | `/{id}/result` | 批阅结果(当前版本 + 历史版本含各版评语) |
|
||||||
|
|
||||||
|
**通知 `/api/notifications`(3 端点)**:GET 分页(含未读数)、`POST /{id}/read`、`POST /read-all`。
|
||||||
|
|
||||||
|
**辅助域(已存在,继续沿用)**:文件 `/api/file/Upload|UploadImage|Down|Preview|Delete|List|Bind`;字典 `/api/dict/List|ManageList|Add|Update|Delete|Sort|Sync`;联系组 `/api/contacts/*`;`/api/base/GetUserInfo|GetUserMod|GetSchoolAll|GetGroupUser|FileUpload|Logout`。
|
||||||
|
|
||||||
|
**V1 冻结接口 `/api/homework/*`(17 个)**:继续服务未切换页面(如「已收到」收件箱、「全部作业」管理端、「作业统计」),M5 完成后评估退役。
|
||||||
|
|
||||||
|
### 1.2 前端现状盘点与技术债
|
||||||
|
|
||||||
|
页面与数据源核对结果(`homeworkApi.*` 实际调用 vs 后端真伪):
|
||||||
|
|
||||||
|
| 页面 | 路由 | 现状数据源 | 判定 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| NewHomework.vue | /list/new | 旧 Create/Edit/GetDetail | 真实接口,需切 V2 |
|
||||||
|
| HomeworkList.vue | /list、/homeworklist | 旧 SendList + **remind(伪)** | 部分伪接口 |
|
||||||
|
| HomeworkDetails.vue | /list/detail | 旧 Detail/GetNextPrev/ReadDetail | 真实接口,需切 V2 |
|
||||||
|
| Submissions.vue | /submissions | **Submissions/SubmissionDetail/grade(伪)** | 全部伪接口 |
|
||||||
|
| Grade.vue | /grade | **sendList + Submissions/SubmissionDetail/grade(伪)** | 部分伪接口 |
|
||||||
|
| StudentHomeworks.vue | /stu/today、/stu/redo、/par/kids | **MyHomeworks/parentNudge(伪)** | 全部伪接口 |
|
||||||
|
| HomeworkAccept.vue | /accept | 旧 List/Detail(v1 收件箱) | 真实接口,需改造为学生详情页 |
|
||||||
|
| HomeworkStats.vue | /stats | 旧 Statis | 真实接口,保留 |
|
||||||
|
| HomeworkSetting.vue | /setting | dict/contacts | 真实接口,保留 |
|
||||||
|
| ApprovalList.vue | /approval | **approvalList/approve/reject(伪)** | 后端无审批域 |
|
||||||
|
| ApprovalSetting.vue | /approval-setting | **approvers 系(伪)** | 后端无审批域 |
|
||||||
|
|
||||||
|
**「伪接口」清单**(后端不存在,真实环境必 404,仅 mock 模式可跑):`/api/homework/` 下的 `Submissions、SubmissionDetail、Grade、Remind、MyHomeworks、MySubmission、Submit、ParentNudge、ApprovalList、ApprovalCount、Approve、Reject`;`/api/dict/` 下的 `ApproverPool、Approvers、AddApprover、UpdateApprover、DelApprover`;`/api/base/SetRole`。
|
||||||
|
|
||||||
|
**类型契约差距**:`types/homework.ts` 为 V1 契约(hwStatus 四态 `draft/collecting/closed/done`、submitStatus 小写五值);V2 要求 hwStatus 五态 `DRAFT_UNPUBLISHED/DRAFT_SCHEDULED/PUBLISHED/CLOSED/ARCHIVED`、submitStatus 五态 `NOT_SUBMITTED/SUBMITTED/LATE_SUBMITTED/RETURNED/REVIEWED`、reviewStatus 三态 `PENDING/REVIEWED/RETURNED`。
|
||||||
|
|
||||||
|
**工作区未提交改动**:Grade/StudentHomeworks/Submissions 三页的图片预览/上传优化 + mock/env 配置微调(与本次切换方向一致,保留并在其上继续)。
|
||||||
|
|
||||||
|
## 2. 总体方案
|
||||||
|
|
||||||
|
三层推进:**契约层(类型+API 客户端)→ 教师端页面 → 学生端页面**,最后清理菜单路由与 mock。旧 V1 页面(已收到/全部作业/统计/设置)不动,继续走冻结接口。
|
||||||
|
|
||||||
|
### 2.1 契约层(P0)
|
||||||
|
|
||||||
|
**新增 `src/types/homeworkV2.ts`**(V2 契约独立成文件,不污染 V1 类型;与后端 `model/v2/*.java` 一一对齐):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 作业五态(PRD §17) */
|
||||||
|
export type HwStatusV2 = "DRAFT_UNPUBLISHED" | "DRAFT_SCHEDULED" | "PUBLISHED" | "CLOSED" | "ARCHIVED";
|
||||||
|
/** 提交五态 */
|
||||||
|
export type SubmitStatusV2 = "NOT_SUBMITTED" | "SUBMITTED" | "LATE_SUBMITTED" | "RETURNED" | "REVIEWED";
|
||||||
|
/** 批阅三态 */
|
||||||
|
export type ReviewStatus = "PENDING" | "REVIEWED" | "RETURNED";
|
||||||
|
|
||||||
|
export interface AttachmentItem { fileId?: number; fileUrl: string; fileType?: "image" | "file"; fileName?: string; sort?: number; }
|
||||||
|
export interface ClassItem { classId: string; className: string; }
|
||||||
|
export interface ProgressVo { should: number; read: number; submitted: number; graded: number; pendingGrade: number; unsubmitted: number; unread: number; unreadUndone: number; readRate: number; submitRate: number; gradeRate: number; }
|
||||||
|
export interface HomeworkV2Vo { id; title; subject; description; hwStatus: HwStatusV2; hwStatusDesc; scheduledAt; publishedAt; deadlineAt; archivedAt; submitType: string[]; allowLate; remind1d; remind1h; classes: ClassItem[]; attachments: AttachmentItem[]; publisherId; publisherName; createTime; updateTime; progress?: ProgressVo | null; }
|
||||||
|
export interface HomeworkV2Request { title: string; subject?; description?; classIds: string[]; attachments?: AttachmentItem[]; deadlineAt?; submitType: string[]; allowLate?; remind1d?; remind1h?; }
|
||||||
|
export interface TeacherHomeworkQuery { pageIndex?; pageSize?; keyword?; subject?; hwStatus?; }
|
||||||
|
export interface StudentMatrixVo / StudentMatrixQuery / HomeworkStatsVo / ClassTreeNodeV2(id: string)/ SubmissionVersionVo / StudentHomeworkV2Vo(含 MySubmission)/ StudentHomeworkDetailV2Vo / StudentSubmitRequest / StudentResultVo / NotificationVo …
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增 3 个 API 客户端**(风格沿 `api/module/homework.ts` 的 `get/postJSON` 封装):
|
||||||
|
|
||||||
|
- `src/api/module/teacherHomework.ts` → `teacherApi`:19 端点全覆盖 + `exportUrl(id)`(Excel 直链,浏览器跳转下载)
|
||||||
|
- `src/api/module/studentHomework.ts` → `studentApi`:6 端点
|
||||||
|
- `src/api/module/notification.ts` → `notificationApi`:3 端点
|
||||||
|
|
||||||
|
**修正 `api/module/homework.ts`**:`baseApi.setRole` 标注仅 mock 可用(后端无此端点,真实环境学生身份由 CAS 登录账号 ↔ class_student 映射,联调用种子账号 stu01~stu06);`baseApi.getClassTree` 的调用方改走 `teacherApi.classTree()`。
|
||||||
|
|
||||||
|
### 2.2 页面改造映射(P1~P3)
|
||||||
|
|
||||||
|
| PRD 页面 | 前端页面 | 目标接口 | 关键改造点 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 页 1 发布作业 | NewHomework.vue | POST/PUT + /classes + publish/schedule + file/Upload | ① 表单重构:classIds(年级→班级树多选,数据源换 `/teacher/homeworks/classes`)② 新增:截止时间(精确到分钟,空=仅查阅型)、submitType(图片/文件/两者/文本)、allowLate、remind1d/remind1h、定时发布时间 ③ 保存语义拆分:「存草稿」(POST/PUT)与「立即发布」(POST 后调 publish)/「定时发布」(POST 后调 schedule)④ 附件走 file/Upload,组装 AttachmentItem[] ⑤ 编辑回显改 GET /teacher/homeworks/{id}(旧 GetDetail 保留给转发场景) |
|
||||||
|
| 页 2 草稿箱 | HomeworkList.vue(/list 增加「草稿箱」段) | GET /drafts | 草稿两态(未发布/定时待发)标签展示;操作:编辑、发布、取消定时(schedule cancel)、删除 |
|
||||||
|
| 页 3 已发布列表 | HomeworkList.vue(/list 主段) | GET /published + remind/extend/copy/archive/export | ① 统计列:应交/已读/已交/**未读未交高亮** + 阅读率/提交率(progress)② 状态筛选 PUBLISHED/CLOSED/ARCHIVED ③ 操作组:提醒(NOT_READ/NOT_SUBMITTED,返回 reminded 提示)、延长截止(日期选择,须晚于当前)、复制为新草稿、归档、导出 Excel(直链)④ 行点击进详情 |
|
||||||
|
| 页 4 详情/统计 | HomeworkDetails.vue | GET /{id} + GET /{id}/stats | 作业信息卡(五态标签、学科、截止、提交方式、班级、附件下载)+ 统计卡片(总进度 + 分班进度:阅读率/提交率/批阅率、待批阅数) |
|
||||||
|
| 页 5 学生矩阵 | Submissions.vue | GET /{id}/students | ① 列:学生/班级/阅读状态(含首末阅读时间)/提交状态(含逾期角标)/提交时间/评分/批阅状态/附件缩略图 ② 筛选:classId、submitStatus、reviewStatus、只看待批阅(pendingGrade)③ 未交学生占位行(submissionId=0)④ 行操作:批阅、打回、提醒单个学生(remind 定向) |
|
||||||
|
| 页 6 批阅/打回/版本 | Grade.vue(批改中心) | /published(pendingGrade 作业列表) + versions + review/return | ① 待批作业列表改 GET /published(按 progress.pendingGrade 排序/展示)② 选中学生(矩阵 pendingGrade=true)→ 右侧工作台 ③ 版本切换(GET versions,默认最新)④ 批阅保存(score+comment 至少一项)⑤ 打回(comment 必填,二次确认)⑥ 提交内容展示:text + 附件/图片(复用已有缩略图放大预览) |
|
||||||
|
| 学生页 1 列表 | StudentHomeworks.vue | GET /student/homeworks?tab= | ① tab 改 all/todo/done/closed(对齐 meta.initTab=redo→todo 语义调整)② 倒计时(deadlineAt)③ my 提交状态徽标(五态 + 逾期角标)④ 分页 + keyword |
|
||||||
|
| 学生页 2 详情/结果 | HomeworkAccept.vue(改造为学生详情页,挂 /stu/detail?serid=) | GET /{id} + read + result | ① 作业说明富文本 + 附件预览/下载 ② **进入页 3 秒定时器调 read(onUnmounted 清理)** ③ 我的提交区(当前版本 text/附件/评分/评语)④ 历史版本 + 各版评语(GET result)⑤ 提交/撤回/重交入口 |
|
||||||
|
| 学生页 3 提交 | HomeworkAccept.vue 内提交区(+列表页快捷弹窗保留) | POST submissions + PUT withdraw/resubmit | ① 按 submitType 动态渲染:文本框 / 图片上传(file/UploadImage 产物→AttachmentItem)/文件上传 ② 截止校验:截止后 allowLate=0 禁交 ③ 撤回(截止前)→ 重新填写重交 ④ 打回(RETURNED)→ 显示订正原因 + 重交 |
|
||||||
|
|
||||||
|
**不动页面**:HomeworkStats(旧 Statis)、HomeworkSetting(dict/contacts)、/accept 的 v1 收件箱语义由 HomeworkAccept 改造承接(路由 `/accept` 重定向到 `/stu/today`)。
|
||||||
|
|
||||||
|
### 2.3 路由与菜单调整(P3)
|
||||||
|
|
||||||
|
| 项 | 处置 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| 新增 `/stu/detail`(HomeworkAccept 改造后) | meta `{ role: "student", title: "作业详情", modId: 33 }` | PRD 学生页 2/3 载体 |
|
||||||
|
| `/accept` | 重定向 `/stu/today` | v1 收件箱语义被学生列表替代 |
|
||||||
|
| `/approval`、`/approval-setting` + 菜单 modId 5/6 | **路由与菜单摘除**(页面文件保留不引) | 后端无审批域(plan-v3 §4.6 顺延),伪接口联调必挂 |
|
||||||
|
| `/par/kids` 家长端 | 保留路由但隐藏入口 | 后端无家长接口(plan-v3 §4.6 顺延),parentNudge 为伪接口;页面内调用加守卫 |
|
||||||
|
| `/homeworklist` 全部作业、`/stats` 统计、`/setting` 设置 | 保留走 V1 冻结接口 | 冻结接口可用,M5 后评估退役 |
|
||||||
|
| 通知中心(可选) | 视排期加 `/notify` 轻量页(notificationApi 3 端点,未读角标) | PRD §18 未列页面,plan-v3 §7.3 补充接口;列为可选项 |
|
||||||
|
|
||||||
|
### 2.4 mock 同步(P4)
|
||||||
|
|
||||||
|
`src/assets/mock/homeworkapi.ts` 增补 V2 端点 mock(teacher 19 + student 6 + notifications 3),数据模型对齐 `types/homeworkV2.ts`,保证 `npm run dev:mock` 全页面可演示;同时清理伪接口 mock(Submissions/Grade/MyHomeworks/审批系),避免继续误导联调。
|
||||||
|
|
||||||
|
## 3. 实施顺序与验收
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 验收标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| P0 契约层 | types/homeworkV2.ts + teacherHomework/studentHomework/notification 三客户端 + homework.ts 修正 | `vue-tsc` 通过;接口路径/参数与本文 §1.1 表逐项一致 |
|
||||||
|
| P1 教师端发布链路 | NewHomework + HomeworkList(草稿箱+已发布) | 真实后端(8102):建作业→存草稿→草稿箱可见→立即发布/定时发布→已发布列表出统计聚合 |
|
||||||
|
| P2 教师端批阅链路 | HomeworkDetails(stats)+ Submissions(矩阵)+ Grade(批阅/打回/版本) | 矩阵筛选/缩略图/占位行正确;批阅→学生端可见结果;打回→重交→版本递增 |
|
||||||
|
| P3 学生端闭环 | StudentHomeworks + HomeworkAccept + 路由菜单调整 | 用种子账号(stu01~stu06):列表 tab→详情 3 秒已读→提交→倒计时/逾期分流→撤回留版本→打回重交→查结果 |
|
||||||
|
| P4 清理与全量走查 | mock 更新、审批/家长端摘除、全页面回归 | dev(dev) + dev:mock 双模式走查;PRD §18 九页面映射逐格核对 |
|
||||||
|
|
||||||
|
每阶段一组语义化 commit;P1 起需要后端 8102 运行 + `sql/v3_seed.sql` 名册种子(库名 `class_homework`,utf8mb4)。
|
||||||
|
|
||||||
|
## 4. 风险与开放问题
|
||||||
|
|
||||||
|
| # | 事项 | 影响 | 处置建议 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 学生身份联调:后端无 SetRole,学生接口按 CAS 登录账号 ↔ class_student 映射 | 高(P3 阻塞) | 与后端确认本地登录方案(种子账号 stu01~stu06 走 CAS 或本地放行);mock 模式保留 SetRole 演示用 |
|
||||||
|
| 2 | 审批域下线需产品确认 | 中 | 本计划先摘菜单/路由不删代码;产品若要保留需后端补接口 |
|
||||||
|
| 3 | 提交附件契约:v2 只收 file/Upload 产物(fileId/fileUrl),拒绝 dataURL | 中 | HomeworkUpload 组件改造为真实上传后再组装 AttachmentItem;改造前 dataURL 直交必被后端拒 |
|
||||||
|
| 4 | 学生端入口与菜单结构(/stu/* 不在现有静态菜单) | 低 | 沿用现有约定,学生视角由平台角色决定入口;具体待 GetUserMod 对接方案 |
|
||||||
|
| 5 | Excel 导出/export 直链需带登录态 cookie | 低 | 同源代理下 `location.href` 直跳即可(与 AllFileDown 同模式) |
|
||||||
|
| 6 | V1 页面(已收到/全部作业/统计)与新数据并存的两套状态口径 | 低 | 双轨期 hwStatus 派生映射(后端已做),前端不做转换;M5 后评估 V1 退役 |
|
||||||
|
| 7 | tab=redo(待订正)语义:V2 无独立 redo 桶,RETURNED 归入 todo | 低 | /stu/redo 路由 initTab 改 todo 并在页内强化 RETURNED 过滤,或改路由为 /stu/todo |
|
||||||
|
|
||||||
|
## 5. 交付物清单
|
||||||
|
|
||||||
|
1. `src/types/homeworkV2.ts` —— V2 全量类型契约
|
||||||
|
2. `src/api/module/teacherHomework.ts` / `studentHomework.ts` / `notification.ts` —— 28 端点客户端
|
||||||
|
3. 改造后 7 个页面:NewHomework / HomeworkList / HomeworkDetails / Submissions / Grade / StudentHomeworks / HomeworkAccept
|
||||||
|
4. `src/router/routes.ts` + `src/assets/config/menu.ts` 调整
|
||||||
|
5. `src/assets/mock/homeworkapi.ts` V2 增补与伪接口清理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 审核(审批)功能下线(2026-09-15)
|
||||||
|
|
||||||
|
### 6.1 裁决变化
|
||||||
|
|
||||||
|
§2.3 与 §4 风险 2 原口径为「摘菜单/路由、页面文件保留不引,等产品确认」。本次确认**彻底删除审批(审核)功能**:产品定位为任课教师直接布置,不存在送审环节;后端 `ClassHomeWork` 从未实现审批域(无 `approval_*` 列、无 `ApprovalList/Approve/Reject`、无审批人池接口),而前端两页调用的 `homeworkApi.approvalList/approve/reject` 与整个 `approvalSettingApi` **在 API 层根本不存在**,属编译期即悬空的死代码。
|
||||||
|
|
||||||
|
注意边界:本节只删「审批/审核」(approval),**不动「批改/批阅」(grade/review)**——`Grade.vue`、`Submissions.vue`、`ReviewModal.vue` 是作业核心链路。
|
||||||
|
|
||||||
|
### 6.2 牵连面盘点(grep `approval|审核|审批|approver|canApprove|vote|flow` 全仓)
|
||||||
|
|
||||||
|
| # | 文件 | 现状 | 处置 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `src/views/homework/ApprovalList.vue` | 审批列表页,未挂路由,调用不存在的 `homeworkApi.approvalList/approve/reject` | 删除(git 跟踪,可 `git checkout` 恢复) |
|
||||||
|
| 2 | `src/views/homework/ApprovalSetting.vue` | 审批人池设置页,未挂路由,调用不存在的 `approvalSettingApi` | 删除(同上) |
|
||||||
|
| 3 | `src/components/ApprovalFlowEditor.vue` | 工作区已处于删除态(`git status` 显示 ` D`) | 保持删除,不恢复 |
|
||||||
|
| 4 | `src/types/homework.ts` | `HomeworkVo` 81-95、`ApprovalStatus` 103、`HomeworkDetailVo` 146-174(含 `approvalFlow`/`canApprove`)、`HomeworkRequest` 393-398(`approvers`/`vote`/`flow`)、`ApprovalQuery` 489、`ApprovalAction` 500、`ApproverVo` 509、分节标题 476 | 全部删除;分节标题改「班级」 |
|
||||||
|
| 5 | `src/api/module/homework.ts` | 23-24 `import { ApprovalQuery, ApproverVo }`(导入后零使用) | 删两行导入 |
|
||||||
|
| 6 | `src/assets/config/menu.ts` | 12-13 静态菜单 `id/modId 5 审批列表 /approval`、`6 审核设置 /approval-setting`(指向不存在的路由) | 删两行 |
|
||||||
|
| 7 | `src/utils/menuRoute.ts` | 无审批别名;但 `resolveMenuPage` 对无法识别的模块只 `console.warn` 并返回 `path:""`,而 `layout.vue:192` 直接渲染 `modList`(不过滤空 path)→ 平台 `GetUserMod` 若仍下发 modId 5/6,侧栏会出现点了没反应的死菜单 | 新增 `DROPPED_MOD_IDS={5,6}` + `dropDroppedModules(list)`(顶层 + children,按 `modId ?? id`,不改入参),`App.vue` 赋值 `layoutData.modList` 前调用 |
|
||||||
|
| 8 | `src/assets/mock/homeworkapi.ts` | 审批引擎与数据:头注释 7/15-16、审批人池 41-52、`buildFlowFromInput` 62-72、`pendingApprovers` 75-79、`canAct` 81-84、`flowProgressText` 86-92、种子 `approval*` 字段 155-313(7 处)、`seedFlows` 318-343、复位循环 345-353、`isPublished` 639-641、`APPROVAL_DESC` 648、列表映射 682-689、详情映射 728-753、Create 1026-1032、Edit 1059-1092 | 全删;`isPublished` 简化为「非草稿即可见」;种子行不再带 `approval*` 键 |
|
||||||
|
| 9 | 其余 `src/views`、`src/components`、`src/router`、`App.vue` | grep **零命中** | 无需改动 |
|
||||||
|
|
||||||
|
后端 `E:\YHXT\java\ClassHomeWork`:审批域从未落地,无可删代码;仅需把 `classwork-homework-plan-v4.md` 的 §0 排除说明与 Q-08 由「待确认」改为「已裁决下线」。
|
||||||
|
|
||||||
|
### 6.3 执行步骤与验收
|
||||||
|
|
||||||
|
1. 备份:`types/homework.ts`、`api/module/homework.ts`、`assets/mock/homeworkapi.ts`、`assets/config/menu.ts`、`utils/menuRoute.ts`、`App.vue` 快照到 `.backup/20260915-drop-approval/`(这些文件工作区相对 HEAD 已有 M5 未提交改动,`git restore` 会连带丢改动,故单独快照)。
|
||||||
|
2. 删 2 个页面文件(`git rm` 语义,历史可查)。
|
||||||
|
3. 按 6.2 顺序改 types → api → menu → menuRoute+App → mock(自底向上,先断引用再清数据)。
|
||||||
|
4. 验收(实测见 §6.4):① `grep -rniE "approval|approver|canApprove" src` 命中数为 0;② `npm run build` 成功;③ `dev:mock` 下逐端点冒烟正常、审批端点 404、侧栏无审批入口。
|
||||||
|
> 原定的 `npx vue-tsc --noEmit` 门禁在本工程**跑不起来**:`vue-tsc@1.8.27` 与 `typescript@5.9.3` 不兼容会直接崩溃,且工程 tsc 基线本就有 65 条存量报错。故类型侧只作「不得新增引用已删符号的报错」的对照检查,见 §6.4 第 4 条。
|
||||||
|
5. 结果回填 §6.4。
|
||||||
|
|
||||||
|
### 6.4 执行结果回填(2026-09-15 完成)
|
||||||
|
|
||||||
|
**改动清单**
|
||||||
|
|
||||||
|
| 文件 | 动作 |
|
||||||
|
|---|---|
|
||||||
|
| `src/views/homework/ApprovalList.vue`、`ApprovalSetting.vue` | `git rm`(索引内 `D`,历史可查) |
|
||||||
|
| `src/components/ApprovalFlowEditor.vue` | 保持工作区已删除状态,未恢复 |
|
||||||
|
| `src/types/homework.ts` | 删 `HomeworkVo` 8 个 approval 字段、`ApprovalStatus`、`HomeworkDetailVo` 9 个 approval 字段(含 `approvalFlow`/`canApprove`)、`HomeworkRequest` 的 `approvers/vote/flow`、`ApprovalQuery`/`ApprovalAction`/`ApproverVo`;分节标题「班级 / 审批」→「班级」 |
|
||||||
|
| `src/api/module/homework.ts` | 删 `ApprovalQuery`、`ApproverVo` 两个死导入 |
|
||||||
|
| `src/assets/config/menu.ts` | 删 modId 5「审批列表」/6「审核设置」两项,头注释记录「平台若仍下发则由 DROPPED_MOD_IDS 剔除」 |
|
||||||
|
| `src/utils/menuRoute.ts` | 新增 `DROPPED_MOD_IDS={5,6}`、`DROPPED_NAMES`(审批/审核 7 个叫法)、`dropDroppedModules()`(顶层+children,按 `modId ?? id` 或名称命中,返回新数组不改入参) |
|
||||||
|
| `src/App.vue` | `GetUserMod` 结果入壳层前经 `dropDroppedModules` 过滤(侧栏渲染、默认跳转、console.table 共用同一份) |
|
||||||
|
| `src/assets/mock/homeworkapi.ts` | 删审批引擎(审批人池 `approvers/approverSeq/approverName/approverPoolAccounts`、`FlowLevel`、`buildFlowFromInput`、`pendingApprovers`、`canAct`、`flowProgressText`、`APPROVAL_DESC`)、`seedFlows()` 与其后的复位 forEach、种子 8 处 approval 属性、`toVo`/详情两处 8 行映射与 `approvalFlow` 块、Create 新对象的 7 行 approval 初始化、Edit 的 flow 重建整段(35 行);`isPublished()` 由「审批通过或免审」改为 `hwStatus !== "draft"`;头注释与存储注释同步改写 |
|
||||||
|
|
||||||
|
**验收实测**
|
||||||
|
|
||||||
|
1. **残留归零**:`grep -rniE "approval|approver|canapprove|flowprogress|buildflow" src` → **0 命中**(中文「审批/审核」仅存于注释与 `DROPPED_NAMES` 名单,属有意保留)。
|
||||||
|
2. **构建通过**:`npm run build`(`vite build --mode base`)**EXIT=0**,18.82s。
|
||||||
|
3. **mock 冒烟**(`npm run dev:mock` @8077,node fetch 逐端点,全部 `code==0`):
|
||||||
|
- `List` 5/7 条、`SendList?scope=user` 3/3 条、`Detail?serid=1&type=0` 出参键仍为 `serid/Contents/DeptID/...ChaoSend/tlist` 且**不再含任何 approval 键**、`ReadDetail` 按班级分组(`account=1301`/`teaname=初一(1)班(45人)`)、`MyHomeworks` buckets `{todo:3,submitted:1,graded:2,redo:0,all:7}`、`Statis` 11 字段齐全、`GetClassTree` 年级→班级带 `studentCount`。
|
||||||
|
- 审批端点确认消失:`/api/homework/ApprovalList`、`/api/dict/ApproverPool` 均 **HTTP 404**。
|
||||||
|
- 临时服务已按 PID 校验(命令行确为本工程 `vite.js --mode mock`)后 taskkill,8077 已释放。
|
||||||
|
4. **类型检查工具链本身不可用(既有问题,非本次引入)**:`vue-tsc@1.8.27` 在 `typescript@5.9.3` 下直接崩溃(`Search string not found: "/supportedTSExtensions = .*(?=;)/"`,vue-tsc 1.x 的 monkey-patch 与 TS 5.5+ 不兼容)。退化用 `npx tsc --noEmit` 得 **65 个报错,全部为存量**:alova 泛型 `TS2344 Type 'T' does not satisfy the constraint 'AlovaGenerics'`(连未改动的 `notification.ts`/`teacherHomework.ts`/`studentHomework.ts` 同样报)、mock 种子数组属性推断 `TS2339`、`menu.ts` 的 `IModule` 未从 `@/store` 导出、`teacherNames` 未使用等;**其中无一条引用被删除的审批符号**(已 grep 验证)。→ 建议后续单列一项:升级 `vue-tsc` 到 2.x 或锁 `typescript<=5.4`,否则类型门禁形同虚设。
|
||||||
|
|
||||||
|
**行为变化(预期内)**:原先 `pending`/`rejected` 的演示种子不再被门控,收件箱从 6 条变 7 条、`Statis.publishedCount` 7/8;这符合「无送审环节,发布即可见」的产品口径。
|
||||||
|
|
||||||
|
**回退方式**:`git checkout HEAD -- src/views/homework/ApprovalList.vue src/views/homework/ApprovalSetting.vue` 可复原页面;types/api/menu/menuRoute/App/mock 六个文件的改动前快照在 `.backup/20260915-drop-approval/`(因工作区相对 HEAD 另有未提交的 M5 改动,直接 `git restore` 会连带丢 M5,故用快照)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 应用设置功能下线(2026-09-15)
|
||||||
|
|
||||||
|
### 7.1 裁决
|
||||||
|
|
||||||
|
「应用设置」(路由 `/setting`,modId 21)承载两块能力:**发文部门字典**(`dict/ManageList·Add·Update·Delete·Sort`)与**自定义联系组**(`contacts/GetGroup·AddGroup·EditGroup·DelGroup·GetContactsTea·SetContactsTea·GetGroupTree`),任课教师布置作业不依赖它们。裁决:
|
||||||
|
|
||||||
|
- **只删前端**:后端 `DictController`/`ContactsController` 及其 `sys_dict`/`contacts_group`/`contacts_group_user` 表**保留不动**(`HomeworkService.resolveDeptName` 仍要查 `sys_dict`;端点无人调用不影响运行)。
|
||||||
|
- **仅该页使用的组件与死 API 一并清掉**(与 §6 同一口径)。
|
||||||
|
|
||||||
|
### 7.2 牵连面盘点
|
||||||
|
|
||||||
|
| 文件 | 现状 | 处置 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/views/homework/HomeworkSetting.vue` | 应用设置页(部门拖拽排序 + 联系组及组员维护) | 删除(git 跟踪) |
|
||||||
|
| `src/components/TypeSetBox.vue` | 仅被 HomeworkSetting 引用(全仓 grep 零其他消费者) | 删除 |
|
||||||
|
| `src/components/UserCheckInput.vue` | 零消费者(其 `loader` 用 `baseApi.getSchoolAll` + `settingApi.getGroupTree`) | 删除 |
|
||||||
|
| `src/router/routes.ts` | `/setting` 路由(86-91)+ 旧路径重定向 `/homework/setting`(121)+ 头注释 `21=应用设置` | 删两处路由与注释项 |
|
||||||
|
| `src/assets/config/menu.ts` | `{ id: 21, modId: 21, name: "应用设置", page: "/setting" }` | 删除 |
|
||||||
|
| `src/api/module/homework.ts` | `class SettingApi`(12 个方法)+ `export const settingApi`;`BaseApi.getSchoolAll`;`HomeworkApi.getSysDic`(dict/List)、`getSchModuleIsSms`(dict/Sync) 自 §6 后零调用 | 全部删除 |
|
||||||
|
| `src/types/homework.ts` | `DeptManageItem`、`DictOption`、`ContactGroupPage`、`ContactUserPage`、`ContactGroupItem`、`ContactUser` | 删除 |
|
||||||
|
| `src/assets/mock/homeworkapi.ts` | `/api/base/GetSchoolAll`(682)、`/api/dict/*`(692-741)、`/api/contacts/*`(1173-1250) 三组桩;种子 `groups/groupSeq`(412-418)、`idNames`(420)、`teacherNames`(425,本就是死码)、`schoolTree`(431)、`deptSeq`(31) | 全部删除;头注释端点清单同步改写 |
|
||||||
|
|
||||||
|
### 7.3 明确保留项(勿顺手删)
|
||||||
|
|
||||||
|
| 保留 | 理由 |
|
||||||
|
|---|---|
|
||||||
|
| mock 的 `deptDict`(21-30) | Create(830)/Edit(867) 仍用 `deptDict.find(d => d.CodeID === b.deptId)` 解析部门名,是作业域数据来源,不是设置页专属 |
|
||||||
|
| `src/components/UserCheckModal.vue`、`ClassCheckInput.vue` | 发作业选班级仍在用 |
|
||||||
|
| `src/components/MyConfirm` | HomeworkList / TeacherHomeworks 仍在用 |
|
||||||
|
| 类型 `SchoolTreeNode` | `BaseApi.getUserInfo` 的返回类型仍引用它 |
|
||||||
|
| `vuedraggable` 依赖 | 删除后成未用依赖,但改 `package.json`/lock 属额外风险,本次不动(后续清依赖时一并处理) |
|
||||||
|
| 后端 `dict/*`、`contacts/*` 端点与三张表 | 按 7.1 裁决保留 |
|
||||||
|
|
||||||
|
### 7.4 对后端 V4 方案的连带影响(需同步改 `classwork-homework-plan-v4.md`)
|
||||||
|
|
||||||
|
1. **E-01 修法调整**:原计划「`deptId` 为空时回填字典默认发文部门」失去维护入口,改为**从当前教师主组/`BoaUser.getCurrentRole()` 推导** `deptId/deptName`,`sys_dict` 仅作为已有数据的名称解析(`resolveDeptName` 保持)。
|
||||||
|
2. **E-05 面收窄但过滤器仍必需**:`postForm` 的 14 处调用点里,dict 4 处 + contacts 6 处随本功能下线归零,剩余 `Remind`(N-04 新门面)、`ReceiveUsers`、`UserPull`、`SetTopmost`、`Expedite`、`InitializeRed`(无参) —— `SingleValueParameterFilter` 照旧要做,只是回归面变小。
|
||||||
|
|
||||||
|
### 7.5 执行步骤与验收
|
||||||
|
|
||||||
|
1. 快照受影响文件到 `.backup/20260915-drop-app-setting/`。
|
||||||
|
2. `git rm` 三个文件 → 删路由/菜单 → 删 SettingApi 与死方法 → 删 types → 清 mock(端点 + 专属种子)。
|
||||||
|
3. 验收:① `grep -rniE "settingApi|DeptManageItem|ContactGroup|ContactUser|DictOption|getSchoolAll|getGroupTree|TypeSetBox|UserCheckInput|homework_setting" src` 命中 0;② `npm run build` EXIT=0;③ `dev:mock` 冒烟:`List/SendList/Detail/ReadDetail/MyHomeworks/Statis/GetClassTree` 仍全 `code=0`,`/api/dict/*`、`/api/contacts/*`、`/api/base/GetSchoolAll` 均 404,`Create` 仍能解析 `deptName`(回归 §7.3 第一条);④ 侧栏无「应用设置」。
|
||||||
|
4. 结果回填 §7.6。
|
||||||
|
|
||||||
|
### 7.6 执行结果回填(2026-09-15 完成)
|
||||||
|
|
||||||
|
**改动清单**
|
||||||
|
|
||||||
|
| 文件 | 动作 |
|
||||||
|
|---|---|
|
||||||
|
| `src/views/homework/HomeworkSetting.vue`、`src/components/TypeSetBox.vue`、`src/components/UserCheckInput.vue` | `git rm` |
|
||||||
|
| `src/router/routes.ts` | 删 `/setting` 路由 + `/homework/setting` 重定向(目标已不存在,留着会把用户导向未知路由);头注释 modId 对照去掉 `21=应用设置` |
|
||||||
|
| `src/assets/config/menu.ts` | 删 `{ id: 21, modId: 21, 应用设置 }`,头注释改为「不做审批、不开放应用设置」 |
|
||||||
|
| `src/utils/menuRoute.ts` | `DROPPED_MOD_IDS` 扩为 `{5, 6, 21}`,`DROPPED_NAMES` 增「应用设置 / 作业设置」——平台 `GetUserMod` 若仍下发 modId 21,侧栏不会出现死菜单 |
|
||||||
|
| `src/api/module/homework.ts` | 删 `class SettingApi`(12 方法)与 `settingApi` 导出、`BaseApi.getSchoolAll`、`HomeworkApi.getSysDic`/`getSchModuleIsSms`、`DICT`/`CONTACTS` 前缀常量、4 个类型导入;头注释第 3 条改写 |
|
||||||
|
| `src/types/homework.ts` | 删 `DictOption`、`DeptManageItem`、`ContactGroupItem`、`ContactGroupPage`、`ContactUser`、`ContactUserPage`;分节改名为「基础平台」(保留 `SchoolTreeNode`) |
|
||||||
|
| `src/assets/mock/homeworkapi.ts` | 删 `dict` 6 桩 + `contacts` 7 桩 + `GetSchoolAll` 桩;删种子 `groups/groupSeq`、`idNames`、`teacherNames`(本就是死码)、`schoolTree`、`deptSeq`;头注释与 `deptDict` 注释同步改写。**`deptDict` 按 §7.3 保留** |
|
||||||
|
|
||||||
|
**验收实测**
|
||||||
|
|
||||||
|
1. **残留归零**:手写代码中 `settingApi|DeptManageItem|ContactGroup|ContactUser|DictOption|getSchoolAll|getGroupTree|TypeSetBox|UserCheckInput|homework_setting|deptSeq` **0 命中**。另有 12 处 grep 命中经逐条核对均非残留:`src/api/apiDefinitions.ts` 与 `globals.d.ts` 是 `npm run api:gen` 生成的 swagger 快照(镜像后端现状,后端端点仍在,属正确内容);其余为 `SchoolTreeNode` 被大小写不敏感的 `schoolTree` 模式误命中。
|
||||||
|
2. **构建通过**:`npm run build` **EXIT=0**,16.79s。
|
||||||
|
3. **mock 冒烟**(`dev:mock` @8077):保留面 9 个端点全 `code=0`(`List` 5/7、`SendList` 3/3、`Detail`、`ReadDetail` 3 组、`MyHomeworks` buckets `{todo:3,submitted:1,graded:2,redo:0,all:7}`、`Statis`、`GetClassTree`、`GetUserInfo`、`GetUserMod`);**§7.3 保留项回归**:`Create(deptId:"D002")` → `GetDetail` 仍解析出 `DeptName="数学教研组"`、`DeptID="D002"`,说明 `deptDict` 未误删;已删端点 6/6 全部 **404**(`dict/List`、`dict/ManageList`、`dict/Sync`、`contacts/GetGroup`、`contacts/GetGroupTree`、`base/GetSchoolAll`)。临时服务按 PID 命令行确认归属后 taskkill,8077 已释放。
|
||||||
|
4. **类型侧**:沿用 §6.4 第 4 条结论(`vue-tsc@1.8.27` 与 `typescript@5.9.3` 不兼容、工程 tsc 基线 65 条存量错),本次只核对「无引用已删符号的新报错」。
|
||||||
|
|
||||||
|
**遗留(有意不动)**:`vuedraggable` 依赖失去唯一使用者,留待统一清依赖时处理;`apiDefinitions.ts`/`globals.d.ts` 待下次 `npm run api:gen` 自然刷新;后端 `dict/*`、`contacts/*` 与三张表按裁决保留。
|
||||||
|
|
||||||
|
**回退方式**:`git checkout HEAD -- src/views/homework/HomeworkSetting.vue src/components/TypeSetBox.vue src/components/UserCheckInput.vue` 复原三文件;routes/menu/menuRoute/api/types/mock 六个文件的改动前快照在 `.backup/20260915-drop-app-setting/`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. M5 主体执行结果回填(2026-09-16 完成)
|
||||||
|
|
||||||
|
### 8.1 提交序列(每阶段一组语义化 commit)
|
||||||
|
|
||||||
|
| commit | 阶段 | 内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| e6639f7 | 基线(前会话成果入库) | P0 契约层(types/homeworkV2.ts + 28 端点三客户端)+ §6/§7 下线 + 新增 TeacherHomeworks/ReviewModal/AttachmentUpload 组件(未挂路由) |
|
||||||
|
| 39824fb | P1 教师端发布链路 | /list 挂 TeacherHomeworks(已发布+草稿箱);NewHomework 切 V2(班级树/create→publish·schedule 三段式/AttachmentUpload/仅查阅互斥;转发保留 V1 GetDetail);HomeworkList 摘伪接口 remind;路由:/accept→/stu/today、新增 /stu/detail、/stu/redo initTab=todo |
|
||||||
|
| 23d6716 | P2 教师端批阅链路 | ReviewWorkbench 从 ReviewModal 抽出(版本切换/批阅/打回共用);Submissions 切 V2 矩阵(stats 汇总卡+classId/submitStatus/pendingGrade 筛选+未交占位行定向提醒);Grade 切 V2 批改中心(pendingGrade 排序+队列+内嵌工作台批完前移);HomeworkDetails 双模式(/list/detail 走 V2 详情+统计+分班进度表,/homeworklist/detail 保留 V1——V2 详情有 requireOwned 校验看不了他人作业) |
|
||||||
|
| 6258ebb | P3 学生端闭环 | StudentHomeworks 切 V2(四 tab 服务端分页+五态徽标+逾期角标+倒计时变色+todo 桶 RETURNED 置顶强化+快捷提交弹窗);HomeworkAccept 改造为学生详情页(3 秒阅读上报 onUnmounted 清理/我的提交/批阅结果/历史版本各版评语/submitType 动态表单/修改提交生成新版本/截止前撤回/打回重交);菜单与别名列齐 V2 |
|
||||||
|
| 44ace7c | P4 清理+mock+走查 | 见 §8.3 |
|
||||||
|
|
||||||
|
### 8.2 关键实现口径(与计划的差异说明)
|
||||||
|
|
||||||
|
1. **页 2/3 载体**:原计划「HomeworkList.vue 增加 草稿箱 段」,实际采用前会话已建好的独立页 TeacherHomeworks.vue 挂 /list(已发布/草稿箱双 Tab),HomeworkList.vue 仅保留给 /homeworklist(全部作业 V1 冻结)。功能映射不变,页面文件归属有变。
|
||||||
|
2. **HomeworkDetails 双模式**(计划外新增):后端 V2 `detail()` 有 `requireOwned` 归属校验,而 /homeworklist 管理页要看他人作业 → 同一组件按路由分支:`/list/detail` 走 V2(详情+stats),`/homeworklist/detail` 走 V1 冻结 Detail。
|
||||||
|
3. **学生端 redo 桶**(§4 风险 7 落地):采用「initTab 改 todo + 页内 RETURNED 红色强化置顶(当前页内重排)」;未做服务端 redo 过滤(V2 无此桶)。
|
||||||
|
4. **修改提交语义**:后端 `doSubmit` 对「已交未批」无拦截,POST 直接生成新版本——修改提交无需先撤回;撤回(withdraw)是独立动作(截止前,生成 NOT_SUBMITTED 空行后重新 POST)。
|
||||||
|
5. **已截止且 allowLate=0 的 RETURNED**:前端在列表与详情均禁重交入口(后端 doSubmit 必拒,前后端口径一致)。
|
||||||
|
6. **vite 代理**:发现工作区实际指向远程测试环境,按计划改回 `http://localhost:8102`(远程地址保留注释可切回)。
|
||||||
|
|
||||||
|
### 8.3 P4 清理与 mock
|
||||||
|
|
||||||
|
- **V1 客户端收敛**(api/module/homework.ts):仅保留仍被消费的 9 方法(sendList/modify/getHomeworkDetails/getNextPrevHomework/orgizeModify/deleteHomework/statistics/fileDownUrl/allFileDownUrl);删除伪接口方法(Submissions/SubmissionDetail/Grade/Remind/MyHomeworks/MySubmission/Submit)与孤儿方法(list/initializeRed/create/editHomework/receiveUsers/userPull/setTopmost/expedite + baseApi.getClassTree + postForm 辅助)。
|
||||||
|
- **types/homework.ts**:收敛为 V1 冻结契约,删 11 个孤儿类型(HomeworkPageResult/InitializeRedVo/HomeworkRequest/SubmitStatus/SubmissionBrief/SubmissionGroup/SubmissionsResultVo/GradeResult/SubmissionDetailVo/GradeRequest/ClassTreeNode/v2 学生端 6 类型等)。
|
||||||
|
- **删除被切换孤儿化的组件**:HomeworkUpload(消费者全部改 AttachmentUpload)、ClassCheckInput、UserCheckModal(ClassCheckInput 唯一消费者)。模板遗留死组件(BschImport/MyTree/TablePage 系等 M5 前已死)未动,留待统一清理。
|
||||||
|
- **mock 整体重写**(.backup/20260916-m5-v2-mock/ 有改动前快照):「V2 单一存储 + V1 适配层」——V2 端点 28 个直连存储(教师 19/学生 6/通知 3),V1 冻结 7 端点(SendList/Detail/GetDetail/GetNextPrev/ReadDetail/Delete/Statis)由同一存储派生(hwStatus 派生映射),与后端「同一张 homework 表」一致;提交内核对齐后端 doSubmit(版本翻转、逾期 LATE_SUBMITTED 分流、image/attach 必附附件、dataURL 拒绝、仅查阅拒绝、归档拒交),submit/action 返回 `{submissionId}`(走查中发现裸 id 契约偏差后修正,教师 create/copy 为裸 Integer 与后端一致);种子 12 份作业覆盖五态(含 DRAFT_SCHEDULED/ARCHIVED),演示学生定制七种提交态(含 hw12 双版本)。
|
||||||
|
|
||||||
|
### 8.4 验收实测
|
||||||
|
|
||||||
|
1. **dev:mock 冒烟(node fetch 逐端点,.backup/20260916-m5-v2-mock/smoke.mjs)**:**58/58 PASS**——教师 23(创建→编辑→定时→取消→发布→详情→统计→矩阵→筛选→提醒(含定向)→延长→复制→删除→批阅→版本→打回→导出占位)、学生 16(四 tab/详情/read/纯文本与带附件提交/withdraw/resubmit/版本递增/仅查阅拒绝/打回态)、通知 4(含 read-all 后 unreadCount=0)、V1 冻结 7、已删伪接口 7 端点全部 404。
|
||||||
|
2. **构建**:`npm run build` EXIT=0(约 40s)。`npx tsc --noEmit` 47 条全部为存量(基线 65 条 alova 泛型等,见 §6.4-4;vue-tsc 工具链不可用为既有问题),**零条引用已删符号**。
|
||||||
|
3. **浏览器走查(dev:mock @8077,IAB)**:9 页面渲染与数据流全过——/list(14 列统计聚合+草稿箱+定时标签)、/list/new(V2 表单)、/list/detail(统计卡+分班表)、/submissions(矩阵+筛选+未交占位)、/grade(队列 31 份+内嵌工作台)、/homeworklist 与其 detail、/stats(V1 face 由 V2 存储派生)、/stu/today(五态徽标+倒计时+身份自动切换)、/stu/detail(提交表单/双版本历史/打回原因)。**交互闭环**:批阅弹窗填 95 分+评语→保存→toast→矩阵行变「已批阅 95」;/stu/detail 停留 >3 秒→教师矩阵 isRead 0→1(firstReadAt=当前时间)。
|
||||||
|
4. **走查发现并修复 3 个缺陷**(均在 44ace7c):
|
||||||
|
- TeacherHomeworks 操作列 Dropdown 在 render 函数下插槽失效(view-ui-plus 需模板插槽),菜单项内联常显 → 改 column.slot + template #action;
|
||||||
|
- ReviewModal 残留 Vue2 写法 `:value/@input`(view-ui-plus Modal 是 modelValue),弹窗打不开 → 修正后批阅闭环通过;
|
||||||
|
- StudentHomeworks「重新提交」按钮缺 canSubmit 门控(已截止且不允许补交时后端必拒)→ 补门控。
|
||||||
|
5. **真实后端(8102)E2E 未执行**:本地后端未启动(MySQL/v3_seed 名册环境未就绪),风险 1 的种子账号联调(stu01~stu06)留待后端联调时进行;vite 代理已指向 8102,冒烟与走查均在 mock 模式完成。
|
||||||
|
|
||||||
|
### 8.5 遗留与后续
|
||||||
|
|
||||||
|
- 真实后端全链路联调(P1~P3 验收的 8102 实测部分):启动 ClassHomeWork + v3_seed.sql 后按 §3 表逐项补测。
|
||||||
|
- 通知中心页面(/notify 轻量页)为可选项未实施;notificationApi 3 端点已就绪并 mock 覆盖。
|
||||||
|
- 模板遗留死组件与 `vuedraggable` 依赖统一清理(§7.6 遗留合并处理)。
|
||||||
|
- 工作区新增用户文档 `bug检测与修复.md`(并行检测计划,44ace7c 随快照入库);`.gitignore` 增补 `.backup/` 忽略。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
//铃铛debug
|
||||||
|
window.conf = {
|
||||||
|
baseHost: "http://basedebug.bestsch.com",
|
||||||
|
ssoHost: "http://ssodebug.bestsch.com",
|
||||||
|
serviceHost: "http://basedebug.bestsch.com/QualityEvaluate",
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# 课程作业模块 · 功能清单与原型说明(v1 / 任课教师视角 · 核心闭环)
|
||||||
|
|
||||||
|
> 说明:本模块只复用校园公告模板的**外壳**——顶栏 + 左栏菜单机制、登录/SSO、班级树选人、富文本、附件上传、列表分页、统计页骨架。内部页面与数据按“课程作业”真实业务重搭,不再套用公告的“发布→接收→已读→审批”模型。
|
||||||
|
>
|
||||||
|
> v1 范围:**仅任课教师端核心闭环** = 布置作业 → 我布置的 → 提交情况(已交/未交) → 批改中心 → 作业统计。
|
||||||
|
> v1 暂不做:作业审核(审批流)、分层/周期性作业、学生端与家长端、拍照/附件真实上传(批改读取用 mock)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 角色与左栏菜单(v1 = 教师视角)
|
||||||
|
|
||||||
|
登录身份 = 任课教师(沿用 `GetUserMod` 数据驱动左栏)。左栏菜单项:
|
||||||
|
|
||||||
|
| 序号 | 菜单名 | 路由 | 对应页面 | 复用/新建 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | 布置作业 | `/assign` | 新建/编辑作业表单 | 改造自现 `NewHomework.vue` |
|
||||||
|
| 2 | 我布置的 | `/list` | 作业列表(按状态分组) | 改造自现 `HomeworkList.vue` |
|
||||||
|
| 3 | 提交情况 | `/submissions` | 单作业的已交/未交名单 | 新建(可并入详情) |
|
||||||
|
| 4 | 批改中心 | `/grade` | 逐份批改工作台 | 新建 |
|
||||||
|
| 5 | 作业统计 | `/stats` | 提交率/正确率/分布 | 改造自现 `HomeworkStats.vue` |
|
||||||
|
|
||||||
|
> “我布置的”行内入口:查看详情、编辑、复制、置顶、撤回、催交;从某条作业进入「提交情况」和「批改中心」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 核心业务闭环(状态机)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────── 我布置的(列表,筛选/状态标签) ────────────┐
|
||||||
|
│ │
|
||||||
|
布置作业(草稿) ─发布→ 收集中 ──到截止──> 已截止 ──全部批改完──> 已完成
|
||||||
|
│ │ │
|
||||||
|
│ 催交(未交) 批改中心(逐份 打分/评语/驳回)
|
||||||
|
│ │ │
|
||||||
|
└──── 编辑/复制/置顶/撤回 订正 ── 学生重交 ── 再批改
|
||||||
|
```
|
||||||
|
|
||||||
|
- **作业状态**:草稿 / 收集中 / 已截止 / 已完成(v1 去掉 pending/rejected 审批态)。
|
||||||
|
- **提交状态**(每个学生×每份作业):未交 / 已交(待批改) / 已批改 / 待订正 / 已订正。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 页面详规
|
||||||
|
|
||||||
|
### 2.1 布置作业(`/assign`)
|
||||||
|
目的:教师创建并即时发布一份作业给若干班级。
|
||||||
|
|
||||||
|
表单项:
|
||||||
|
- 学科(下拉,来自作业设置/字典)
|
||||||
|
- 作业标题(必填)
|
||||||
|
- 布置班级(多选,复用 `ClassCheckInput` 班级树;支持“全部班级”)
|
||||||
|
- 截止时间(日期时间;用于“已截止”判定与倒计时)
|
||||||
|
- 提交方式(多选/单选:文本、图片、附件、无需提交仅查阅)——决定学生端能力与批改界面
|
||||||
|
- 作业正文(富文本,复用 `@wangeditor`)
|
||||||
|
- 附件/题目(复用现附件上传组件 `HomeworkUpload`)
|
||||||
|
- 是否开启短信提醒(复用 `smsFlag`)
|
||||||
|
- 操作:保存草稿 / 发布
|
||||||
|
|
||||||
|
交互要点:发布即进入“收集中”,生成面向所选班级的可见作业;编辑草稿不改变状态。v1 无审批直发。
|
||||||
|
|
||||||
|
API:`POST /api/homework/Create`、`POST /api/homework/Edit`(复用,字段扩展见 §3)。
|
||||||
|
|
||||||
|
### 2.2 我布置的(`/list`)
|
||||||
|
目的:教师管理自己发布的所有作业。
|
||||||
|
|
||||||
|
- 顶部状态标签页:全部 / 收集中 / 已截止 / 已完成 / 草稿
|
||||||
|
- 列表列:标题、学科、布置班级、截止时间、提交进度(已交 x/应交 y)、批改进度(已批 m/已交 n)、状态、操作
|
||||||
|
- 操作:查看详情、编辑、复制、置顶、撤回、催交、进入批改
|
||||||
|
- 支持关键词/学科/时间筛选(复用 `applyFilters` 思路)
|
||||||
|
|
||||||
|
API:`GET /api/homework/SendList?scope=user`(改造为“我布置的”,返回体新增 progress 字段)。
|
||||||
|
|
||||||
|
### 2.3 提交情况(`/submissions?serid=`)
|
||||||
|
目的:看某份作业的收交名单。
|
||||||
|
|
||||||
|
- 头部:作业标题、截止倒计时、提交率、已交/未交/待订正 汇总
|
||||||
|
- 名单(按班级分组,复用 `ReadDetail` 的年级→班级分组):每个学生 提交时间 / 状态 / 快捷“批改”“催交”
|
||||||
|
- 批量:对未交一键催交(短信/站内)
|
||||||
|
|
||||||
|
API:`GET /api/homework/Submissions?serid=`(新增,替代现 `ReadDetail` 的“已读/未读”语义)。
|
||||||
|
|
||||||
|
### 2.4 批改中心(`/grade?serid=&subId=`)
|
||||||
|
目的:逐份高效批改。
|
||||||
|
|
||||||
|
- 左:提交列表(已交待批优先);右:某学生提交内容(文本/图片/附件预览,复用 `@open-file-viewer`/pdfjs)
|
||||||
|
- 批改区:评分方式二选一——分数(0–100)或等级(A/B/C 或 优/良/待改进);文字评语;(可选)图片圈划标注
|
||||||
|
- 动作:保存批改、标为优秀作业、驳回要求订正、(批量)一键通过
|
||||||
|
- 键盘快捷键上一条/下一条,提升批改效率
|
||||||
|
|
||||||
|
API:`GET /api/homework/SubmissionDetail?subId=`、`POST /api/homework/Grade`(新增)。
|
||||||
|
|
||||||
|
### 2.5 作业统计(`/stats`)
|
||||||
|
目的:教师看学情。
|
||||||
|
|
||||||
|
- 单作业维度:提交率、按时率、批改进度、分数/等级分布、订正完成率、共性易错点(v1 可仅展示分布)
|
||||||
|
- 总览维度:我布置的作业数、平均提交率、待批改数量
|
||||||
|
- 图表复用现统计页骨架
|
||||||
|
|
||||||
|
API:`GET /api/homework/Statis`(改造为作业口径)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据模型(v1 新增字段,mock 需对齐)
|
||||||
|
|
||||||
|
**Homework(作业)**
|
||||||
|
- 新增:`subject`(学科)、`deadline`(截止)、`submitType`(提交方式数组)、`status ∈ {draft, collecting, closed, done}`
|
||||||
|
- 去除/停用:`approvalEnabled/approvalStatus/approvalFlow`(v1 不用)、`classRead`(改为按提交统计)
|
||||||
|
|
||||||
|
**Submission(提交)** — 新概念
|
||||||
|
```
|
||||||
|
{ id, homeworkId, studentId, studentName, classId, className,
|
||||||
|
submittedAt|null, submitStatus: 'unsubmitted'|'submitted'|'graded'|'needRedo'|'redone',
|
||||||
|
text, images[], attachments[],
|
||||||
|
grade: { score|null, level|null, comment, isExcellent, markedImages[] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
学生名单来源:班级 → 学生(现 mock 只有班级维度 `studentCount`,v1 需扩到**学生个体**才能做提交/批改;生成每班的 N 个 mock 学生即可)。
|
||||||
|
|
||||||
|
**统计**:提交率=已交/应交;按时率=截止前交/应交;批改进度=已批/已交;分数分布分桶。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 与现有“公告换皮”代码的差异(改造清单)
|
||||||
|
|
||||||
|
| 现有(公告语义) | v1 课程作业 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| 已收到 / 已发送 | 我布置的(+提交情况) | 改列表口径与字段 |
|
||||||
|
| 已读 / 未读(班级维度) | 已交 / 未交(学生个体维度) | 提交名单需下到学生 |
|
||||||
|
| 审批 pending/rejected | v1 不做 | 关闭审批入口与状态 |
|
||||||
|
| 收件箱(学生视角) | v1 不做(属学生端) | 暂隐藏 |
|
||||||
|
| NewHomework/Detail/Stats | 布置作业/详情/统计 | 改造复用 |
|
||||||
|
| ClassCheckInput/富文本/附件/TablePage | 同名复用 | 直接用 |
|
||||||
|
|
||||||
|
Mock 层:在 `src/assets/mock/homeworkapi.ts` 里 ①补 `subject/deadline/submitType`;②加学生个体表;③新增端点 `Submissions / SubmissionDetail / Grade / Remind`;④`Statis` 改作业口径;⑤去掉 `SeedFlows/approval` 相关种子与分支(或保留但不进菜单)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. v1 之后(预留,不实现,仅登记)
|
||||||
|
|
||||||
|
- 学生端 / 家长端(今日作业、提交、看批改、订正补交、提醒)
|
||||||
|
- 作业审核(教研组/教务抽查,复用现审批流,可选开启)
|
||||||
|
- 分层作业(A/B 组不同题量)、周期性作业(每周固定模板自动生成)
|
||||||
|
- 拍照提交 + 手写圈划批改、错题本、年级作业负担看板、优秀作业库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 建议下一步
|
||||||
|
|
||||||
|
1. 你按本页圈改功能/字段(尤其 §2 每页要哪些、§3 提交/批改字段够不够)。
|
||||||
|
2. 确认后我先改造 `mock`(补学生与提交/批改数据 + 新端点),再逐页替换 `views`。
|
||||||
|
3. 先做「布置作业 → 我布置的 → 提交情况」这条能看到数据的链,批改中心随后。
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<contextPath>/api/env-config
|
||||||
|
(后端免登录端点,projman 配置管理在线维护),替代生产构建环境变量。
|
||||||
|
必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发
|
||||||
|
(根路径,段数不足 2)则不设置:本地走 .env.dev/.env.mock(VITE_*),线上走同域推导兜底。
|
||||||
|
target = URL 第 2 段目录名:本应用产物部署于 /{contextPath}/front/,故为 front,
|
||||||
|
对应后端配置键 yh.env-config.source.front(环境变量 YH_ENV_CONFIG_SOURCE_FRONT)。
|
||||||
|
注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var seg = location.pathname.split("/").filter(Boolean);
|
||||||
|
if (seg.length < 2) return; // 本地开发(根路径):走 .env.dev / .env.mock
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false);
|
||||||
|
xhr.send(null);
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var conf = JSON.parse(xhr.responseText);
|
||||||
|
if (conf && typeof conf === "object") window.__APP_CONFIG__ = conf;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* 拉取失败保持未设置,走同域推导兜底 */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<title><%- title %></title>
|
||||||
|
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
||||||
|
<script src="./libs/jquery.min.js"></script>
|
||||||
|
<script src="./libs/lodash.min.js"></script>
|
||||||
|
<script src="./libs/ztree/jquery.ztree.all.min.js"></script>
|
||||||
|
<%- injectScript %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Generated
+12915
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "course-homework",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"nodeversion": "20.19.4",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --mode dev",
|
||||||
|
"dev:mock": "vite --mode mock",
|
||||||
|
"build": "vite build --mode base",
|
||||||
|
"build:base": "vite build --mode base",
|
||||||
|
"build:ld": "vite build --mode ld",
|
||||||
|
"build:prod": "vite build --mode prod",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"api:gen": "alova gen -f"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@open-file-viewer/core": "^0.1.43",
|
||||||
|
"@open-file-viewer/vue": "^0.1.43",
|
||||||
|
"@types/lodash": "^4.14.196",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"alova": "^3.4.0",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"node-sass": "latest",
|
||||||
|
"pdfjs-dist": "^4.10.38",
|
||||||
|
"pinia": "^2.1.4",
|
||||||
|
"sass": "^1.64.1",
|
||||||
|
"view-ui-plus": "^1.3.16",
|
||||||
|
"vite-plugin-mock": "2.9.6",
|
||||||
|
"vue": "^3.4.19",
|
||||||
|
"vue-router": "4",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@alova/wormhole": "^1.5.0",
|
||||||
|
"@types/node": "^20.4.4",
|
||||||
|
"@unocss/preset-rem-to-px": "^66.6.0",
|
||||||
|
"@unocss/preset-uno": "^66.6.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"fast-glob": "^3.3.2",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"unocss": "^66.6.0",
|
||||||
|
"unplugin-auto-import": "0.18.3",
|
||||||
|
"vite": "^5.1.4",
|
||||||
|
"vite-plugin-html": "^3.2.2",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vue-tsc": "^1.8.27"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
àRCopyright 1990-2009 Adobe Systems Incorporated.
|
||||||
|
All rights reserved.
|
||||||
|
See ./LICENSEáCNS2-H
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
àRCopyright 1990-2009 Adobe Systems Incorporated.
|
||||||
|
All rights reserved.
|
||||||
|
See ./LICENSEá ETen-B5-H` ^
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user