|
|
@@ -0,0 +1,237 @@
|
|
|
+import type { Router } from 'vue-router';
|
|
|
+
|
|
|
+/** 快捷入口一项:数据来自侧栏子菜单(叶子路由) */
|
|
|
+export interface QuickEntryMenuOption {
|
|
|
+ id: string;
|
|
|
+ title: string;
|
|
|
+ iconClass: string;
|
|
|
+ /** 与菜单 web_path 一致,用于 router.push */
|
|
|
+ path: string;
|
|
|
+ /** 可选:无 path 时用组件路径匹配(一般不用) */
|
|
|
+ viewImportHints?: string[];
|
|
|
+}
|
|
|
+
|
|
|
+/** 首页快捷入口格子数量(一排展示个数) */
|
|
|
+export const QUICK_ENTRY_SLOT_COUNT = 5;
|
|
|
+
|
|
|
+/** 升级后重新按默认五项初始化(修复旧版 path 匹配不到课表/全部用户时乱序填充的问题) */
|
|
|
+export const QUICK_ENTRY_STORAGE_KEY = 'screenconsole_quick_entry_v2';
|
|
|
+
|
|
|
+export interface QuickEntryPersistShape {
|
|
|
+ /** 与 QuickEntryMenuOption.id 一致,当前实现为路由 path */
|
|
|
+ menuIds: string[];
|
|
|
+ highlightIndex: number;
|
|
|
+}
|
|
|
+
|
|
|
+/** 侧栏 meta.icon → 快捷卡片 `<i class="...">` */
|
|
|
+export function mapSidebarIconToQuickClass(icon?: string): string {
|
|
|
+ if (!icon || !String(icon).trim()) return 'fa fa-file-text-o';
|
|
|
+ return icon.trim();
|
|
|
+}
|
|
|
+
|
|
|
+function resolveMenuTitle(item: RouteItem, translateTitle: (key: string) => string): string {
|
|
|
+ const key = item.meta?.title;
|
|
|
+ if (key) return translateTitle(key);
|
|
|
+ if (typeof item.name === 'string' && item.name) return item.name;
|
|
|
+ return item.path || '';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 收集侧栏中可点击的叶子菜单(与 Vertical 菜单展示逻辑一致:有子则只下钻,无子则为页面)
|
|
|
+ */
|
|
|
+export function collectLeafMenuItems(routes: RouteItem[]): RouteItem[] {
|
|
|
+ const out: RouteItem[] = [];
|
|
|
+
|
|
|
+ for (const item of routes) {
|
|
|
+ if (item.meta?.isHide) continue;
|
|
|
+
|
|
|
+ const path = item.path || '';
|
|
|
+ if (path === '/home' || path === '/') continue;
|
|
|
+
|
|
|
+ /* 新窗口外链,与侧栏 a 标签行为一致,不参与 router 快捷跳转 */
|
|
|
+ if (item.meta?.isLink && !item.meta?.isIframe) continue;
|
|
|
+
|
|
|
+ const rawChildren = item.children || [];
|
|
|
+ const children = rawChildren.filter((c) => !c.meta?.isHide);
|
|
|
+
|
|
|
+ if (children.length > 0) {
|
|
|
+ out.push(...collectLeafMenuItems(children));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 仍有子项但均被隐藏:不把父级当成可跳转页 */
|
|
|
+ if (rawChildren.length > 0 && children.length === 0) continue;
|
|
|
+
|
|
|
+ if (!path) continue;
|
|
|
+
|
|
|
+ out.push(item);
|
|
|
+ }
|
|
|
+
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从当前项目 routesList(已按侧栏过滤)生成快捷入口下拉选项
|
|
|
+ */
|
|
|
+export function buildQuickMenuOptionsFromRoutes(
|
|
|
+ routes: RouteItem[],
|
|
|
+ translateTitle: (key: string) => string
|
|
|
+): QuickEntryMenuOption[] {
|
|
|
+ const leaves = collectLeafMenuItems(routes);
|
|
|
+ const byPath = new Map<string, RouteItem>();
|
|
|
+
|
|
|
+ for (const item of leaves) {
|
|
|
+ if (!item.path || byPath.has(item.path)) continue;
|
|
|
+ byPath.set(item.path, item);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Array.from(byPath.values()).map((item) => ({
|
|
|
+ id: item.path,
|
|
|
+ path: item.path,
|
|
|
+ title: resolveMenuTitle(item, translateTitle),
|
|
|
+ iconClass: mapSidebarIconToQuickClass(item.meta?.icon),
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+/** 与后端 web_path 无关的小写归一化,仅用于片段匹配 */
|
|
|
+function normalizeWebPathForMatch(path: string): string {
|
|
|
+ return path.toLowerCase().replace(/\/+/g, '/').replace(/\/$/, '');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 首次进入首页、且无本地缓存时的默认五个快捷项(顺序固定):
|
|
|
+ * 借用列表 → 设备列表 → 课表管理 → 全部用户 → 设备维修
|
|
|
+ * 通过 path 片段匹配当前用户可见菜单中的对应页,避免写死完整 web_path。
|
|
|
+ */
|
|
|
+export function pickPreferredQuickSlotIds(opts: QuickEntryMenuOption[]): string[] {
|
|
|
+ const used = new Set<string>();
|
|
|
+ const out: string[] = [];
|
|
|
+
|
|
|
+ const pick = (predicate: (normPath: string) => boolean) => {
|
|
|
+ const o = opts.find((x) => !used.has(x.id) && predicate(normalizeWebPathForMatch(x.path)));
|
|
|
+ if (o) {
|
|
|
+ out.push(o.id);
|
|
|
+ used.add(o.id);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** path 或侧栏展示标题(已 i18n)任一命中即可,避免后端 web_path 不含英文片段时匹配失败 */
|
|
|
+ const pickPathOrTitle = (pathOk: (p: string) => boolean, titleOk: (title: string) => boolean) => {
|
|
|
+ const o = opts.find(
|
|
|
+ (x) =>
|
|
|
+ !used.has(x.id) &&
|
|
|
+ (pathOk(normalizeWebPathForMatch(x.path)) || titleOk((x.title || '').trim()))
|
|
|
+ );
|
|
|
+ if (o) {
|
|
|
+ out.push(o.id);
|
|
|
+ used.add(o.id);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 1. 借用列表(主列表页,排除审批/流程等子模块)
|
|
|
+ pickPathOrTitle(
|
|
|
+ (p) =>
|
|
|
+ (p.includes('borrow/index') || /\/borrow$/i.test(p)) &&
|
|
|
+ !p.includes('approval') &&
|
|
|
+ !p.includes('workflow') &&
|
|
|
+ !p.includes('processcreate') &&
|
|
|
+ !p.includes('borrowingnotice'),
|
|
|
+ (t) => t.includes('借用') || t.includes('借还')
|
|
|
+ );
|
|
|
+
|
|
|
+ // 2. 设备列表(排除维修、分类、标签等其它 device-* 页)
|
|
|
+ pickPathOrTitle(
|
|
|
+ (p) =>
|
|
|
+ (p.includes('device/index') || /\/device$/.test(p)) &&
|
|
|
+ !p.includes('devicemaintenance') &&
|
|
|
+ !p.includes('deviceclass') &&
|
|
|
+ !p.includes('devicepreserve') &&
|
|
|
+ !p.includes('devicelabel') &&
|
|
|
+ !p.includes('devicedamage') &&
|
|
|
+ !p.includes('devicemanual'),
|
|
|
+ (t) => t === '设备列表' || (t.includes('设备') && t.includes('列表') && !t.includes('维修'))
|
|
|
+ );
|
|
|
+
|
|
|
+ // 3. 课表管理(path 常见:timetablemanage / timetable / schedule)
|
|
|
+ pickPathOrTitle(
|
|
|
+ (p) =>
|
|
|
+ p.includes('timetable') ||
|
|
|
+ p.includes('schedule') ||
|
|
|
+ p.includes('curriculum') ||
|
|
|
+ p.includes('课表'),
|
|
|
+ (t) => t.includes('课表') || /schedule|timetable/i.test(t)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 4. 全部用户
|
|
|
+ pickPathOrTitle(
|
|
|
+ (p) =>
|
|
|
+ p.includes('allusers') ||
|
|
|
+ p.includes('all-user') ||
|
|
|
+ ((p.includes('/users') || p.endsWith('/users')) &&
|
|
|
+ !p.includes('studentinfo') &&
|
|
|
+ !p.includes('deptuser') &&
|
|
|
+ !p.includes('role') &&
|
|
|
+ !p.includes('account')),
|
|
|
+ (t) => t.includes('全部用户') || /all\s*users/i.test(t)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 5. 设备维修
|
|
|
+ pickPathOrTitle(
|
|
|
+ (p) => p.includes('devicemaintenance') || p.includes('device/maintenance'),
|
|
|
+ (t) => t.includes('设备维修') || /device\s*(maintenance|repair)/i.test(t)
|
|
|
+ );
|
|
|
+
|
|
|
+ while (out.length < QUICK_ENTRY_SLOT_COUNT) {
|
|
|
+ const fill = opts.find((o) => !used.has(o.id));
|
|
|
+ if (!fill) break;
|
|
|
+ out.push(fill.id);
|
|
|
+ used.add(fill.id);
|
|
|
+ }
|
|
|
+
|
|
|
+ return out.slice(0, QUICK_ENTRY_SLOT_COUNT);
|
|
|
+}
|
|
|
+
|
|
|
+export function loadQuickEntryPrefs(): QuickEntryPersistShape {
|
|
|
+ try {
|
|
|
+ const raw = localStorage.getItem(QUICK_ENTRY_STORAGE_KEY);
|
|
|
+ if (!raw) return { menuIds: [], highlightIndex: 0 };
|
|
|
+ const parsed = JSON.parse(raw) as QuickEntryPersistShape;
|
|
|
+ return {
|
|
|
+ menuIds: Array.isArray(parsed.menuIds) ? parsed.menuIds : [],
|
|
|
+ highlightIndex: typeof parsed.highlightIndex === 'number' ? parsed.highlightIndex : 0,
|
|
|
+ };
|
|
|
+ } catch {
|
|
|
+ return { menuIds: [], highlightIndex: 0 };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function saveQuickEntryPrefs(data: QuickEntryPersistShape) {
|
|
|
+ localStorage.setItem(QUICK_ENTRY_STORAGE_KEY, JSON.stringify(data));
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析可跳转 path(优先菜单 path,其次组件路径 hint 兜底)
|
|
|
+ */
|
|
|
+export function resolvePathForQuickOption(router: Router, option: QuickEntryMenuOption): string | null {
|
|
|
+ if (option.path) {
|
|
|
+ try {
|
|
|
+ const resolved = router.resolve(option.path);
|
|
|
+ if (resolved.matched.length) return option.path;
|
|
|
+ } catch {
|
|
|
+ /* 继续 hint 匹配 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const hints = (option.viewImportHints || []).map((h) => h.replace(/^\//, '').replace(/\.vue$/i, ''));
|
|
|
+ if (!hints.length) return null;
|
|
|
+ for (const r of router.getRoutes()) {
|
|
|
+ if (r.meta?.isHide) continue;
|
|
|
+ const comp = r.components?.default as unknown;
|
|
|
+ if (typeof comp === 'function') {
|
|
|
+ const str = Function.prototype.toString.call(comp);
|
|
|
+ for (const hint of hints) {
|
|
|
+ if (str.includes(hint)) return r.path;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|