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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user