Files
UserclassFront/src/utils/menuRoute.ts
T
lijing 7e49587c10 feat: 迁移 Vue3 + Vite + view-ui-plus(对齐 NoticeFront 框架)
- 构建:Vue CLI 4 → Vite 5,入口迁至根 index.html(含 env-config + 主题通道 loader)
- 技术栈:Vue 2.6 → Vue 3.4,view-design → view-ui-plus 1.3.24,vue-router 3 → 4(hash)
- 壳层重写:App.vue / components/layout.vue 对齐 Notice 骨架(logo-badge、icon-* 通道图标、resolveMenuPage、crumb-home 基线)
- 机械转换:具名 slot → template #x、.sync → v-model:、@click.native、$set、beforeDestroy、/deep/ → :deep()
- render:h('Button')/h('Input') → 真组件 + 扁平 props/onClick
- 组件适配:vuedraggable 4 的 #item 插槽;vue-treeselect → @zanmato/vue3-treeselect;Page 分页改 v-model + pageSize 显式回写
- 清理:删除 Vue2 死代码(layout_old/bak/child、App.Net、codeModal、未引用组件)
- 修复:深链接首次加载被重定向到首个模块(router.isReady)

验证:vite build 通过;headless CDP 全 15 路由无异常;教师列表 2 行渲染、操作列按钮点击弹窗正常。
2026-10-20 15:16:52 +08:00

147 lines
5.1 KiB
TypeScript

import routes from "@/router/routes";
/**
* 平台模块配置 → 前端内部路由的统一解析器(对齐 NoticeFront @/utils/menuRoute)。
* 平台配置的 path 格式不可控(旧前缀 / index.html#/hash / 完整 URL / 配错),
* 侧边栏跳转与高亮、登录后默认跳转都必须先经过这里,禁止手拼路由。
*/
const ROUTE_PATHS: string[] = routes
.map((r: any) => (typeof r.redirect === "string" ? r.redirect : r.path))
.filter((p: any): p is string => !!p && !String(p).includes(":"));
const ROUTE_BY_MODID = new Map<number, string>();
for (const r of routes as any[]) {
const modId = r.meta?.modId as number | undefined;
if (!modId || !r.path || String(r.path).includes(":") || r.redirect) continue;
if (!ROUTE_BY_MODID.has(modId)) ROUTE_BY_MODID.set(modId, r.path);
}
const ROUTE_BY_TITLE = new Map<string, string>();
for (const r of routes as any[]) {
if (r.redirect || !r.path || String(r.path).includes(":")) continue;
const title = r.meta?.title as string | undefined;
if (title && !ROUTE_BY_TITLE.has(title)) ROUTE_BY_TITLE.set(title, r.path);
}
/** 平台历史模块名 → 路由(旧菜单叫法与现页面标题不一致时的对照) */
const NAME_ALIASES: Record<string, string> = {
学制设置: "/studySystemChoice",
时段管理: "/itemSet",
学校管理: "/schoolSetting",
课程管理: "/subjectManForZy",
学科设置: "/subjectSetzhi",
班级列表: "/classManage",
选修班: "/electiveClassManage",
教师列表: "/teacherManage",
学生列表: "/studentManage",
日志查询: "/dailySearch",
};
/** 本应用历史上下文前缀(/bschclass、/userclass、/BschClassSystem)→ 去掉后比对 */
const LEGACY_PREFIXES = ["/bschclass", "/userclass", "/bschclasssystem"];
const EXTERNAL_RE = /^(https?:)?\/\//i;
function extractCandidate(raw: string): string {
let p = raw.trim();
try {
p = decodeURIComponent(p);
} catch {
/* 非法编码保持原样 */
}
const hashIdx = p.indexOf("#");
if (hashIdx >= 0) {
p = p.slice(hashIdx + 1);
} else if (EXTERNAL_RE.test(p)) {
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;
}
function matchRoute(candidate: string): string | null {
if (!candidate) return null;
const lower = candidate.toLowerCase();
const exact = ROUTE_PATHS.find((p: string) => p.toLowerCase() === lower);
if (exact) return exact;
for (const prefix of LEGACY_PREFIXES) {
if (lower.startsWith(prefix + "/")) {
const stripped = lower.slice(prefix.length);
const hit = ROUTE_PATHS.find((p: string) => p.toLowerCase() === stripped);
if (hit) return hit;
}
}
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;
}
function isAppEntryCandidate(candidate: string): boolean {
if (!candidate || candidate === "/") return true;
return LEGACY_PREFIXES.some(p => candidate === p);
}
export interface ResolvedMenuPage {
path: string;
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 };
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;
}
const fallback = byName() ?? byModId();
if (fallback) return { path: fallback, external: false };
console.warn("[menuRoute] 模块未配置路径且名称/modId 无法识别:", name, "modId =", modId);
return EMPTY_MENU_PAGE;
}