瀏覽代碼

首页调整

qaz 3 天之前
父節點
當前提交
7cff2987fa
共有 2 個文件被更改,包括 461 次插入294 次删除
  1. 224 294
      src/views/system/screenconsole/index.vue
  2. 237 0
      src/views/system/screenconsole/quickEntry.ts

文件差異過大導致無法顯示
+ 224 - 294
src/views/system/screenconsole/index.vue


+ 237 - 0
src/views/system/screenconsole/quickEntry.ts

@@ -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;
+}

部分文件因文件數量過多而無法顯示