@deppon/create-deppon-app 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +63 -0
- package/deppon.js +640 -0
- package/package.json +51 -0
- package/template/.env +12 -0
- package/template/.env.dev-local.example +64 -0
- package/template/.env.development.example +64 -0
- package/template/.env.example +1 -0
- package/template/.env.production.example +64 -0
- package/template/.env.test.example +64 -0
- package/template/.eslintignore +2 -0
- package/template/.eslintrc.cjs +14 -0
- package/template/.prettierrc.js +3 -0
- package/template/.vscode/settings.json +8 -0
- package/template/Dockerfile +5 -0
- package/template/README.md +149 -0
- package/template/commitlint.config.js +11 -0
- package/template/gitignore +8 -0
- package/template/index.html +18 -0
- package/template/nginx.conf +70 -0
- package/template/npmrc +2 -0
- package/template/package.json +49 -0
- package/template/preview-server.js +117 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logo.png +0 -0
- package/template/src/App.vue +123 -0
- package/template/src/api/index.ts +13 -0
- package/template/src/api/prefercenter.ts +23 -0
- package/template/src/api/product.ts +16 -0
- package/template/src/api/user.ts +41 -0
- package/template/src/components/ExpandableMessage.vue +340 -0
- package/template/src/components/PageLayout.vue +43 -0
- package/template/src/config/dictionaryConfig.ts +24 -0
- package/template/src/directives/permission.ts +162 -0
- package/template/src/layouts/BaseLayout.vue +687 -0
- package/template/src/main.ts +27 -0
- package/template/src/router/index.ts +179 -0
- package/template/src/router/route.ts +61 -0
- package/template/src/stores/menu.ts +334 -0
- package/template/src/stores/product.ts +155 -0
- package/template/src/stores/route.ts +79 -0
- package/template/src/stores/user.ts +145 -0
- package/template/src/styles/index.ts +29 -0
- package/template/src/types/dictionary.d.ts +24 -0
- package/template/src/types/vite-env.d.ts +119 -0
- package/template/src/utils/dictionary.ts +188 -0
- package/template/src/utils/errorAnalyzer.ts +217 -0
- package/template/src/utils/messageVNode.ts +15 -0
- package/template/src/utils/request.ts +293 -0
- package/template/src/views/error/401.vue +30 -0
- package/template/src/views/error/403.vue +30 -0
- package/template/src/views/error/404.vue +30 -0
- package/template/src/views/home/index.vue +25 -0
- package/template/tsconfig.json +27 -0
- package/template/vite.config.ts +243 -0
- package/template/yarnrc +3 -0
- package/template/yarnrc.yml +7 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { setCookieDomain, initIframeStyleFix } from '@deppon/deppon-utils';
|
|
3
|
+
import { VuePlugin as pinia } from '@deppon/deppon-pinia';
|
|
4
|
+
import router from './router';
|
|
5
|
+
import { VuePlugin as request } from '@deppon/deppon-request';
|
|
6
|
+
import App from './App.vue';
|
|
7
|
+
import { vPermission } from './directives/permission';
|
|
8
|
+
|
|
9
|
+
// ==================== 样式导入 ====================
|
|
10
|
+
// 统一导入所有组件样式,确保构建时样式被正确打包
|
|
11
|
+
import './styles/index';
|
|
12
|
+
|
|
13
|
+
// ==================== iframe 样式修复 ====================
|
|
14
|
+
// 在 iframe 嵌套时,修复外部壳子页面的 CSS 样式 fix uap css
|
|
15
|
+
initIframeStyleFix();
|
|
16
|
+
|
|
17
|
+
// ==================== 应用初始化 ====================
|
|
18
|
+
const app = createApp(App);
|
|
19
|
+
// 初始化 cookie 域名
|
|
20
|
+
setCookieDomain();
|
|
21
|
+
// 注册权限指令
|
|
22
|
+
app.directive('permission', vPermission);
|
|
23
|
+
// 安装 Request、Pinia、Router
|
|
24
|
+
app.use(request)
|
|
25
|
+
.use(pinia)
|
|
26
|
+
.use(router as any)
|
|
27
|
+
.mount('#app');
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createAuthGuard, parseToken, resolveRoutes, type MenuNode } from '@deppon/deppon-auth';
|
|
2
|
+
import { createDepponRouter, type Router } from '@deppon/deppon-router';
|
|
3
|
+
import NProgress from 'nprogress';
|
|
4
|
+
import 'nprogress/nprogress.css';
|
|
5
|
+
import { useRouteStore } from '../stores/route';
|
|
6
|
+
import { useUserStore } from '../stores/user';
|
|
7
|
+
import { useProductStore } from '../stores/product';
|
|
8
|
+
import { staticRoutes, developmentRoutes } from './route';
|
|
9
|
+
|
|
10
|
+
const viewModules = import.meta.glob('../views/**/*.vue');
|
|
11
|
+
const routerBase = import.meta.env.DP_PUBLIC_PATH || '/';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 计算节点的 path(与 pathTransformer 逻辑保持一致)
|
|
15
|
+
*/
|
|
16
|
+
const getNodePath = (node: MenuNode): string => {
|
|
17
|
+
if (node.uri === null) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
return node.uri || node.path || `/${node.id}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 过滤菜单节点,排除 path 为 '/' 的节点
|
|
25
|
+
*/
|
|
26
|
+
const filterMenuNodes = (nodes: MenuNode[]): MenuNode[] => {
|
|
27
|
+
return nodes.filter(node => {
|
|
28
|
+
const uri = node.uri;
|
|
29
|
+
const path = node.path;
|
|
30
|
+
|
|
31
|
+
if (uri === '/' || path === '/') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (uri === null) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const finalPath = getNodePath(node);
|
|
40
|
+
const trimmedPath = finalPath.trim();
|
|
41
|
+
return trimmedPath !== '/' && trimmedPath !== '';
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const resolveMenuRoutes = (menuTree: MenuNode[]) =>
|
|
46
|
+
resolveRoutes(menuTree, {
|
|
47
|
+
componentResolver: path => {
|
|
48
|
+
const normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
49
|
+
return viewModules[`../views/${normalized}.vue`];
|
|
50
|
+
},
|
|
51
|
+
metaTransformer: node => ({
|
|
52
|
+
title: node.text || node.name,
|
|
53
|
+
icon: node.iconCls || node.icon,
|
|
54
|
+
hidden: node.hidden === false ? false : node.hidden || false,
|
|
55
|
+
}),
|
|
56
|
+
pathTransformer: node => getNodePath(node),
|
|
57
|
+
nameTransformer: node => node.text || node.name || node.id,
|
|
58
|
+
skipNoComponent: true,
|
|
59
|
+
}).filter(route => route.component);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 加载动态路由
|
|
63
|
+
*/
|
|
64
|
+
const loadDynamicRoutes = async () => {
|
|
65
|
+
const routeStore = useRouteStore();
|
|
66
|
+
|
|
67
|
+
if (routeStore.isRoutesLoaded) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (routeStore.hasMenuTree) {
|
|
71
|
+
appendDynamicRoutes(routeStore.menuTree);
|
|
72
|
+
routeStore.setRoutesLoaded(true);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// TODO: 从服务端获取菜单树
|
|
77
|
+
// const response = await getMenu(0);
|
|
78
|
+
// if (response.success) {
|
|
79
|
+
// const allNodes: MenuNode[] = response?.nodes || [];
|
|
80
|
+
// const menuTree: MenuNode[] = filterMenuNodes(allNodes);
|
|
81
|
+
// if (menuTree.length > 0) {
|
|
82
|
+
// routeStore.setMenuTree(menuTree);
|
|
83
|
+
// appendDynamicRoutes(menuTree);
|
|
84
|
+
// }
|
|
85
|
+
// }
|
|
86
|
+
routeStore.setRoutesLoaded(true);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getUserStore = () => {
|
|
90
|
+
try {
|
|
91
|
+
return useUserStore();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn('[deppon-auth] 获取用户 Store 失败,请确认 Pinia 是否已初始化', error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const authGuard = createAuthGuard({
|
|
99
|
+
enableLoginCheck: import.meta.env.DP_ENABLE_LOGIN_CHECK === 'true',
|
|
100
|
+
cookieKeys: ['TGC', '_TOKENUUMS', 'U'],
|
|
101
|
+
checkLogin: cookies => {
|
|
102
|
+
return Boolean(cookies.TGC && cookies._TOKENUUMS);
|
|
103
|
+
},
|
|
104
|
+
onRequireLogin: () => {
|
|
105
|
+
getUserStore()?.clearAuth();
|
|
106
|
+
if (import.meta.env.DEV) {
|
|
107
|
+
window.open(import.meta.env.DP_LOGIN_URL || '/', '_blank');
|
|
108
|
+
} else {
|
|
109
|
+
window.location.href = import.meta.env.DP_LOGIN_URL || '/';
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
whiteList: ['/404'],
|
|
113
|
+
showProgress: true,
|
|
114
|
+
progressBar: {
|
|
115
|
+
start: () => NProgress.start(),
|
|
116
|
+
done: () => NProgress.done(),
|
|
117
|
+
},
|
|
118
|
+
onLoggedIn: async cookies => {
|
|
119
|
+
const token = cookies._TOKENUUMS;
|
|
120
|
+
if (!token) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const profile = parseToken(token);
|
|
124
|
+
const userStore = getUserStore();
|
|
125
|
+
const routeStore = useRouteStore();
|
|
126
|
+
|
|
127
|
+
const previousToken = userStore?.token;
|
|
128
|
+
const isUserChanged = previousToken && previousToken !== token;
|
|
129
|
+
const isRelogin = !previousToken && token;
|
|
130
|
+
|
|
131
|
+
if (isUserChanged || isRelogin) {
|
|
132
|
+
routeStore.clearMenuTree();
|
|
133
|
+
routeStore.setRoutesLoaded(false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
userStore?.setAuth({ profile, token });
|
|
137
|
+
|
|
138
|
+
// 初始化产品信息
|
|
139
|
+
const productStore = useProductStore();
|
|
140
|
+
// 如果用户切换或重新登录,清空产品信息并重新加载
|
|
141
|
+
if (isUserChanged || isRelogin) {
|
|
142
|
+
productStore.clearProductInfo();
|
|
143
|
+
}
|
|
144
|
+
productStore.fetchProductInfo();
|
|
145
|
+
|
|
146
|
+
await loadDynamicRoutes();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 根据环境决定使用的路由
|
|
151
|
+
const isDevOrTest =
|
|
152
|
+
import.meta.env.MODE === 'development' || import.meta.env.MODE === 'test' || import.meta.env.MODE === 'dev-local';
|
|
153
|
+
const routes = isDevOrTest ? [...developmentRoutes, ...staticRoutes] : staticRoutes;
|
|
154
|
+
|
|
155
|
+
const router = createDepponRouter({
|
|
156
|
+
routes,
|
|
157
|
+
guards: {
|
|
158
|
+
beforeEach: authGuard.beforeEach,
|
|
159
|
+
afterEach: authGuard.afterEach,
|
|
160
|
+
},
|
|
161
|
+
routerOptions: {
|
|
162
|
+
history: 'web',
|
|
163
|
+
base: routerBase,
|
|
164
|
+
scrollBehavior: () => ({ top: 0 }),
|
|
165
|
+
} as any,
|
|
166
|
+
}) as Router;
|
|
167
|
+
|
|
168
|
+
const appendDynamicRoutes = (menuTree: MenuNode[]) => {
|
|
169
|
+
const dynamicRoutes = resolveMenuRoutes(menuTree);
|
|
170
|
+
dynamicRoutes.forEach(route => {
|
|
171
|
+
const routeName = route.name as string;
|
|
172
|
+
if (!router.hasRoute(routeName)) {
|
|
173
|
+
router.addRoute(route);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default router;
|
|
179
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import BaseLayout from '@/layouts/BaseLayout.vue';
|
|
3
|
+
// 统一使用 BaseLayout 作为父级容器(iframe 测试请使用 public/iframe-test.html)
|
|
4
|
+
|
|
5
|
+
// 根据环境变量判断是否使用 BaseLayout
|
|
6
|
+
// DP_USE_LAYOUT 默认为 true(测试环境),false 表示 iframe 模式(生产环境)
|
|
7
|
+
const useLayout = import.meta.env.DP_USE_LAYOUT !== 'false';
|
|
8
|
+
|
|
9
|
+
// 静态路由(生产环境使用)
|
|
10
|
+
export const staticRoutes: RouteRecordRaw[] = [
|
|
11
|
+
{
|
|
12
|
+
path: '/401',
|
|
13
|
+
name: 'Unauthorized',
|
|
14
|
+
component: () => import('../views/error/401.vue'),
|
|
15
|
+
meta: {
|
|
16
|
+
title: '未授权',
|
|
17
|
+
hidden: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: '/403',
|
|
22
|
+
name: 'Forbidden',
|
|
23
|
+
component: () => import('../views/error/403.vue'),
|
|
24
|
+
meta: {
|
|
25
|
+
title: '禁止访问',
|
|
26
|
+
hidden: true,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: '/404',
|
|
31
|
+
name: 'NotFound',
|
|
32
|
+
component: () => import('../views/error/404.vue'),
|
|
33
|
+
meta: {
|
|
34
|
+
title: '页面不存在',
|
|
35
|
+
hidden: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: '/:pathMatch(.*)*',
|
|
40
|
+
redirect: '/404',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// 开发环境路由(开发环境和测试环境使用)
|
|
45
|
+
export const developmentRoutes: RouteRecordRaw[] = [
|
|
46
|
+
{
|
|
47
|
+
path: '/',
|
|
48
|
+
component: useLayout ? BaseLayout : undefined,
|
|
49
|
+
redirect: '/home',
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
path: '/home',
|
|
53
|
+
name: 'Home',
|
|
54
|
+
component: () => import('../views/home/index.vue'),
|
|
55
|
+
meta: { title: '首页' },
|
|
56
|
+
},
|
|
57
|
+
// 可以在这里添加开发环境专用的路由
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { defineStore } from '@deppon/deppon-pinia';
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
export interface CommonMenuItem {
|
|
5
|
+
key: string;
|
|
6
|
+
path: string;
|
|
7
|
+
title: string;
|
|
8
|
+
icon?: any;
|
|
9
|
+
timestamp?: number; // 添加时间戳,用于判断最早添加的项
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const COMMON_MENU_STORAGE_KEY = 'deppon-common-menu';
|
|
13
|
+
const ROUTE_HISTORY_STORAGE_KEY = 'deppon-route-history';
|
|
14
|
+
const MAX_HISTORY_COUNT = 20; // 最多保存20条历史记录
|
|
15
|
+
const DEFAULT_MAX_COMMON_MENU_COUNT = 5; // 默认最多显示5个常用菜单
|
|
16
|
+
|
|
17
|
+
export const useMenuStore = defineStore('menu', () => {
|
|
18
|
+
// 常用菜单列表
|
|
19
|
+
const commonMenuItems = ref<CommonMenuItem[]>([]);
|
|
20
|
+
|
|
21
|
+
// 常用菜单最大数量(可动态设置)
|
|
22
|
+
const maxCommonMenuCount = ref<number>(DEFAULT_MAX_COMMON_MENU_COUNT);
|
|
23
|
+
|
|
24
|
+
// 路由访问历史(用于自动填充常用菜单)
|
|
25
|
+
const routeHistory = ref<Array<{ path: string; title: string; timestamp: number }>>([]);
|
|
26
|
+
|
|
27
|
+
// 是否已初始化标志
|
|
28
|
+
const isInitialized = ref<boolean>(false);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从本地存储加载常用菜单
|
|
32
|
+
*/
|
|
33
|
+
const loadCommonMenuFromStorage = () => {
|
|
34
|
+
try {
|
|
35
|
+
const stored = localStorage.getItem(COMMON_MENU_STORAGE_KEY);
|
|
36
|
+
if (stored) {
|
|
37
|
+
const parsed = JSON.parse(stored);
|
|
38
|
+
commonMenuItems.value = Array.isArray(parsed) ? parsed : [];
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
commonMenuItems.value = [];
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 保存常用菜单到本地存储
|
|
47
|
+
*/
|
|
48
|
+
const saveCommonMenuToStorage = () => {
|
|
49
|
+
// 如果还未初始化,不保存(避免在初始化前保存空数据)
|
|
50
|
+
if (!isInitialized.value) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
localStorage.setItem(COMMON_MENU_STORAGE_KEY, JSON.stringify(commonMenuItems.value));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// 静默失败
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 从本地存储加载路由历史
|
|
63
|
+
*/
|
|
64
|
+
const loadRouteHistoryFromStorage = () => {
|
|
65
|
+
try {
|
|
66
|
+
const stored = localStorage.getItem(ROUTE_HISTORY_STORAGE_KEY);
|
|
67
|
+
if (stored) {
|
|
68
|
+
routeHistory.value = JSON.parse(stored);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
routeHistory.value = [];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 保存路由历史到本地存储
|
|
77
|
+
*/
|
|
78
|
+
const saveRouteHistoryToStorage = () => {
|
|
79
|
+
try {
|
|
80
|
+
// 只保留最近的记录
|
|
81
|
+
const recentHistory = routeHistory.value
|
|
82
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
83
|
+
.slice(0, MAX_HISTORY_COUNT);
|
|
84
|
+
localStorage.setItem(ROUTE_HISTORY_STORAGE_KEY, JSON.stringify(recentHistory));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// 静默失败
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 添加路由访问记录
|
|
92
|
+
*/
|
|
93
|
+
const addRouteToHistory = (path: string, title: string) => {
|
|
94
|
+
// 排除一些不需要记录的路径
|
|
95
|
+
const excludePaths = ['/', '/404', '/401', '/403', '/home'];
|
|
96
|
+
if (excludePaths.includes(path)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 检查是否已存在相同路径的记录
|
|
101
|
+
const existingIndex = routeHistory.value.findIndex(item => item.path === path);
|
|
102
|
+
|
|
103
|
+
if (existingIndex >= 0) {
|
|
104
|
+
// 更新现有记录的时间戳
|
|
105
|
+
routeHistory.value[existingIndex].timestamp = Date.now();
|
|
106
|
+
routeHistory.value[existingIndex].title = title;
|
|
107
|
+
} else {
|
|
108
|
+
// 添加新记录
|
|
109
|
+
routeHistory.value.push({
|
|
110
|
+
path,
|
|
111
|
+
title,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 保存到本地存储
|
|
117
|
+
saveRouteHistoryToStorage();
|
|
118
|
+
|
|
119
|
+
// 自动更新常用菜单
|
|
120
|
+
updateCommonMenuFromHistory();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 根据路由历史自动更新常用菜单
|
|
125
|
+
* 根据最新打开的前5个路由进行填充
|
|
126
|
+
* 如果已经在常用菜单中存在,则更新其时间戳
|
|
127
|
+
* 如果常用菜单已满,移除最早添加的项,然后添加新的
|
|
128
|
+
*/
|
|
129
|
+
const updateCommonMenuFromHistory = () => {
|
|
130
|
+
if (routeHistory.value.length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 按时间戳排序,获取最近访问的路由
|
|
135
|
+
const recentRoutes = routeHistory.value.sort((a, b) => b.timestamp - a.timestamp);
|
|
136
|
+
|
|
137
|
+
// 确保常用菜单项有时间戳(兼容旧数据)
|
|
138
|
+
commonMenuItems.value.forEach(item => {
|
|
139
|
+
if (!item.timestamp) {
|
|
140
|
+
item.timestamp = Date.now();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 获取当前常用菜单中已存在的路径映射
|
|
145
|
+
const existingItemsMap = new Map<string, CommonMenuItem>();
|
|
146
|
+
commonMenuItems.value.forEach(item => {
|
|
147
|
+
existingItemsMap.set(item.path, item);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 处理最近访问的路由,最多处理 maxCommonMenuCount 个
|
|
151
|
+
// 使用递增的时间戳,确保最新访问的路由时间戳最大(显示在最前面)
|
|
152
|
+
let baseTimestamp = Date.now();
|
|
153
|
+
const processedRoutes: CommonMenuItem[] = [];
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < recentRoutes.slice(0, maxCommonMenuCount.value).length; i++) {
|
|
156
|
+
const route = recentRoutes[i];
|
|
157
|
+
|
|
158
|
+
// 排除首页
|
|
159
|
+
if (route.path === '/home') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const existingItem = existingItemsMap.get(route.path);
|
|
164
|
+
// 使用递增的时间戳,确保最新访问的路由时间戳最大
|
|
165
|
+
const itemTimestamp = baseTimestamp + (maxCommonMenuCount.value - i);
|
|
166
|
+
|
|
167
|
+
if (existingItem) {
|
|
168
|
+
// 如果已存在,更新其时间戳(使用更大的时间戳,确保显示在最前面)
|
|
169
|
+
existingItem.timestamp = itemTimestamp;
|
|
170
|
+
existingItem.title = route.title; // 更新标题(可能变化)
|
|
171
|
+
processedRoutes.push(existingItem);
|
|
172
|
+
} else {
|
|
173
|
+
// 如果不存在,创建新项
|
|
174
|
+
processedRoutes.push({
|
|
175
|
+
key: `common-${route.path.replace(/\//g, '-')}-${itemTimestamp}`,
|
|
176
|
+
path: route.path,
|
|
177
|
+
title: route.title,
|
|
178
|
+
timestamp: itemTimestamp,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 合并现有项和新项,去重(按路径)
|
|
184
|
+
const mergedMap = new Map<string, CommonMenuItem>();
|
|
185
|
+
// 先添加现有项(保留已有的)
|
|
186
|
+
commonMenuItems.value.forEach(item => {
|
|
187
|
+
mergedMap.set(item.path, item);
|
|
188
|
+
});
|
|
189
|
+
// 再添加新项(会覆盖已存在的,更新其时间戳)
|
|
190
|
+
processedRoutes.forEach(item => {
|
|
191
|
+
mergedMap.set(item.path, item);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 按时间戳排序,最新的在前(时间戳大的在前)
|
|
195
|
+
const sortedItems = Array.from(mergedMap.values()).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
196
|
+
|
|
197
|
+
// 保留最新的 maxCommonMenuCount 个
|
|
198
|
+
commonMenuItems.value = sortedItems.slice(0, maxCommonMenuCount.value);
|
|
199
|
+
|
|
200
|
+
saveCommonMenuToStorage();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 删除常用菜单项
|
|
205
|
+
*/
|
|
206
|
+
const handleCommonMenuDelete = (key: string) => {
|
|
207
|
+
const index = commonMenuItems.value.findIndex(item => item.key === key);
|
|
208
|
+
if (index >= 0) {
|
|
209
|
+
commonMenuItems.value.splice(index, 1);
|
|
210
|
+
saveCommonMenuToStorage();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 手动添加常用菜单项
|
|
216
|
+
*/
|
|
217
|
+
const addCommonMenuItem = (item: CommonMenuItem) => {
|
|
218
|
+
// 排除首页
|
|
219
|
+
if (item.path === '/home') {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 确保有时间戳
|
|
224
|
+
if (!item.timestamp) {
|
|
225
|
+
item.timestamp = Date.now();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 检查是否已存在
|
|
229
|
+
const existingIndex = commonMenuItems.value.findIndex(menu => menu.path === item.path);
|
|
230
|
+
if (existingIndex >= 0) {
|
|
231
|
+
// 如果已存在,更新其时间戳为当前时间(确保显示在最前面)
|
|
232
|
+
commonMenuItems.value[existingIndex].timestamp = Date.now();
|
|
233
|
+
commonMenuItems.value[existingIndex].title = item.title;
|
|
234
|
+
} else {
|
|
235
|
+
// 如果不存在,添加新项,使用当前时间戳(确保显示在最前面)
|
|
236
|
+
item.timestamp = Date.now();
|
|
237
|
+
commonMenuItems.value.push(item);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 按时间戳排序,最新的在前(时间戳最大的在最前面)
|
|
241
|
+
commonMenuItems.value.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
242
|
+
|
|
243
|
+
// 如果超过最大数量,移除最早添加的项(时间戳最小的)
|
|
244
|
+
if (commonMenuItems.value.length > maxCommonMenuCount.value) {
|
|
245
|
+
commonMenuItems.value = commonMenuItems.value.slice(0, maxCommonMenuCount.value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
saveCommonMenuToStorage();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 设置常用菜单最大数量
|
|
253
|
+
*/
|
|
254
|
+
const setMaxCommonMenuCount = (max: number) => {
|
|
255
|
+
maxCommonMenuCount.value = max || DEFAULT_MAX_COMMON_MENU_COUNT;
|
|
256
|
+
// 如果当前菜单数量超过新的最大值,移除多余的
|
|
257
|
+
if (commonMenuItems.value.length > maxCommonMenuCount.value) {
|
|
258
|
+
commonMenuItems.value = commonMenuItems.value.slice(0, maxCommonMenuCount.value);
|
|
259
|
+
saveCommonMenuToStorage();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 初始化:从本地存储加载数据
|
|
265
|
+
*/
|
|
266
|
+
const init = (maxCount?: number) => {
|
|
267
|
+
// 先加载历史记录和常用菜单(在设置最大值之前)
|
|
268
|
+
loadRouteHistoryFromStorage();
|
|
269
|
+
loadCommonMenuFromStorage();
|
|
270
|
+
|
|
271
|
+
// 清理已存在的首页
|
|
272
|
+
const hasExcludedPath = commonMenuItems.value.some(item => item.path === '/home');
|
|
273
|
+
if (hasExcludedPath) {
|
|
274
|
+
commonMenuItems.value = commonMenuItems.value.filter(item => item.path !== '/home');
|
|
275
|
+
saveCommonMenuToStorage();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 如果传入了最大数量,设置它
|
|
279
|
+
if (maxCount !== undefined) {
|
|
280
|
+
setMaxCommonMenuCount(maxCount);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 如果常用菜单为空且历史记录不为空,尝试从历史记录填充
|
|
284
|
+
// 注意:只有在常用菜单完全为空时才填充,避免覆盖已有数据
|
|
285
|
+
if (commonMenuItems.value.length === 0 && routeHistory.value.length > 0) {
|
|
286
|
+
updateCommonMenuFromHistory();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 确保菜单数量不超过最大值(但不要清空已有数据)
|
|
290
|
+
// 只有在超过最大值时才截取,保留最新的
|
|
291
|
+
if (commonMenuItems.value.length > maxCommonMenuCount.value) {
|
|
292
|
+
// 按时间戳排序,保留最新的
|
|
293
|
+
commonMenuItems.value.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
294
|
+
commonMenuItems.value = commonMenuItems.value.slice(0, maxCommonMenuCount.value);
|
|
295
|
+
saveCommonMenuToStorage();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 标记为已初始化,之后可以正常保存
|
|
299
|
+
isInitialized.value = true;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// 计算属性:获取格式化的常用菜单(用于 ProLayout)
|
|
303
|
+
const formattedCommonMenu = computed(() => {
|
|
304
|
+
// 过滤掉首页
|
|
305
|
+
const filteredItems = commonMenuItems.value.filter(item => item.path !== '/home');
|
|
306
|
+
|
|
307
|
+
if (filteredItems.length === 0) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
// 直接使用过滤后的数组,顺序已经正确(最新的在前)
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
type: 'common',
|
|
314
|
+
title: '常用菜单',
|
|
315
|
+
children: filteredItems,
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
commonMenuItems,
|
|
322
|
+
routeHistory,
|
|
323
|
+
formattedCommonMenu,
|
|
324
|
+
maxCommonMenuCount,
|
|
325
|
+
isInitialized,
|
|
326
|
+
addRouteToHistory,
|
|
327
|
+
handleCommonMenuDelete,
|
|
328
|
+
addCommonMenuItem,
|
|
329
|
+
updateCommonMenuFromHistory,
|
|
330
|
+
setMaxCommonMenuCount,
|
|
331
|
+
init,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
|