- 构建: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 行渲染、操作列按钮点击弹窗正常。
147 lines
5.1 KiB
TypeScript
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;
|
|
}
|