feat(M5): P0 契约层 + 审批/应用设置下线
- types/homeworkV2.ts:V2 全量类型契约(五态/枚举/矩阵/版本/通知)
- teacherHomework/studentHomework/notification 三客户端(28 端点)
- §6 审批功能下线:删 ApprovalList/ApprovalSetting/ApprovalFlowEditor,
types/api/menu/menuRoute/App/mock 同步清理,DROPPED_MOD_IDS={5,6}
- §7 应用设置下线:删 HomeworkSetting/TypeSetBox/UserCheckInput,
SettingApi 与 dict/contacts 死代码清理,DROPPED_MOD_IDS 扩 {21}
- 新增 V2 组件:TeacherHomeworks(已发布+草稿箱)、ReviewModal(批阅弹窗)、
AttachmentUpload(附件上传)——尚未挂路由,页面切换随后续阶段
This commit is contained in:
@@ -0,0 +1,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>
|
||||
Reference in New Issue
Block a user