@litianxiang/portal-core 0.1.3 → 0.1.4
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/README.md +108 -14
- package/dist/index.js +14 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# @litianxiang/portal-core 使用说明
|
|
2
2
|
|
|
3
|
-
`@litianxiang/portal-core`
|
|
3
|
+
`@litianxiang/portal-core` 是一套门户子系统通用的路由/认证内核,负责统一处理:
|
|
4
4
|
|
|
5
5
|
- 登录态校验 + SSO 恢复
|
|
6
6
|
- 菜单加载与动态路由挂载
|
|
7
7
|
- 根路径重定向到第一个业务页面
|
|
8
|
-
-
|
|
8
|
+
- 统一退出到认证站、空闲超时判断等通用认证逻辑
|
|
9
9
|
|
|
10
|
-
各子系统只需注入自己的静态路由、用户 Store
|
|
10
|
+
各子系统只需注入自己的静态路由、用户 Store、(可选)Tab Store 和“第一个页面”的计算函数,即可接入。
|
|
11
11
|
|
|
12
12
|
## 安装
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
pnpm add @litianxiang/portal-core
|
|
16
16
|
# 或
|
|
17
|
+
pnpm update @litianxiang/portal-core
|
|
18
|
+
# 或
|
|
17
19
|
npm install @litianxiang/portal-core
|
|
18
20
|
```
|
|
19
21
|
|
|
@@ -24,13 +26,17 @@ npm install @litianxiang/portal-core
|
|
|
24
26
|
|
|
25
27
|
## 导出的能力
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
当前主要导出以下能力:
|
|
28
30
|
|
|
29
31
|
```ts
|
|
30
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createAppRouter,
|
|
34
|
+
createLogoutToAuth,
|
|
35
|
+
calcInactivityAction
|
|
36
|
+
} from '@litianxiang/portal-core'
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
类型签名简化说明:
|
|
39
|
+
`createAppRouter` 类型签名简化说明:
|
|
34
40
|
|
|
35
41
|
```ts
|
|
36
42
|
interface AppRouterOptions {
|
|
@@ -42,6 +48,38 @@ interface AppRouterOptions {
|
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
function createAppRouter(options: AppRouterOptions): Router
|
|
51
|
+
|
|
52
|
+
`auth` 相关工具简化说明:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface LogoutOptions {
|
|
56
|
+
getUserStore: () => any // 返回当前系统的 userStore(需提供 clearUserInfo 等方法)
|
|
57
|
+
getLoadingStore?: () => any // 返回 loadingStore(可选,用于全局 loading 结束)
|
|
58
|
+
authLoginUrl?: string // 统一登录地址(不传则默认 /auth/#/login)
|
|
59
|
+
ssoStorageKey?: string // 统一登录本地缓存 key,默认 'user-store'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 生成一个“退出到统一登录”的函数,一般在 http 拦截器或手动退出按钮中调用
|
|
63
|
+
function createLogoutToAuth(options: LogoutOptions): () => void
|
|
64
|
+
|
|
65
|
+
// 计算基于最后一次操作时间的“是否需要退出/刷新 token”等动作
|
|
66
|
+
interface InactivityOptions {
|
|
67
|
+
expireMs?: number // token 允许的最大空闲时间,默认 2 小时
|
|
68
|
+
bufferMs?: number // 提前刷新 token 的缓冲时间,默认 5 分钟
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function calcInactivityAction(
|
|
72
|
+
lastActiveTime: number | null | undefined,
|
|
73
|
+
now: number,
|
|
74
|
+
options?: InactivityOptions
|
|
75
|
+
): {
|
|
76
|
+
shouldLogout: boolean
|
|
77
|
+
shouldRefresh: boolean
|
|
78
|
+
inactiveMs: number
|
|
79
|
+
expireMs: number
|
|
80
|
+
bufferMs: number
|
|
81
|
+
}
|
|
82
|
+
```
|
|
45
83
|
```
|
|
46
84
|
|
|
47
85
|
## 子系统接入步骤
|
|
@@ -119,11 +157,13 @@ export const useUserStore = defineStore('user', {
|
|
|
119
157
|
})
|
|
120
158
|
```
|
|
121
159
|
|
|
122
|
-
### 3. 准备 Tab Store
|
|
160
|
+
### 3. 准备 Tab Store(可选)
|
|
161
|
+
|
|
162
|
+
Tab Store 主要用于和各系统的 Tab 控件协作,例如在“手动退出”时统一清理 Tab。当前路由内核在菜单加载 / 刷新场景下不会自动清空 Tab。
|
|
123
163
|
|
|
124
|
-
|
|
164
|
+
建议至少包含:
|
|
125
165
|
|
|
126
|
-
- `clearAll()
|
|
166
|
+
- `clearAll()`:清空当前系统的所有 Tab
|
|
127
167
|
|
|
128
168
|
示例:
|
|
129
169
|
|
|
@@ -171,8 +211,61 @@ const router = createAppRouter({
|
|
|
171
211
|
|
|
172
212
|
export default router
|
|
173
213
|
```
|
|
214
|
+
### 6.(可选)接入统一退出与空闲超时控制
|
|
215
|
+
|
|
216
|
+
在各子系统的 http 封装中,可以结合 `createLogoutToAuth` 和 `calcInactivityAction` 实现统一退出与“2 小时未操作自动退出”的逻辑。例如:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// src/utils/http.ts
|
|
220
|
+
import axios from 'axios'
|
|
221
|
+
import { createLogoutToAuth, calcInactivityAction } from '@litianxiang/portal-core'
|
|
222
|
+
import { useUserStore } from '@/stores/user'
|
|
223
|
+
import { useLoadingStore } from '@/stores/loading'
|
|
224
|
+
|
|
225
|
+
const logoutToAuth = createLogoutToAuth({
|
|
226
|
+
getUserStore: () => useUserStore(),
|
|
227
|
+
getLoadingStore: () => useLoadingStore(),
|
|
228
|
+
authLoginUrl: import.meta.env.VITE_AUTH_LOGIN_URL
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
axios.interceptors.request.use(async config => {
|
|
232
|
+
const userStore = useUserStore()
|
|
233
|
+
const { shouldLogout, shouldRefresh } = calcInactivityAction(
|
|
234
|
+
userStore.lastActiveTime,
|
|
235
|
+
Date.now()
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if (shouldLogout) {
|
|
239
|
+
logoutToAuth()
|
|
240
|
+
return Promise.reject(new Error('登录已过期'))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (shouldRefresh) {
|
|
244
|
+
// 此处调用后端刷新 token 接口,成功后更新 userStore 中的 token
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 正常附带 Authorization 头,并更新 lastActiveTime
|
|
248
|
+
if (userStore.getUserAccessToken) {
|
|
249
|
+
config.headers = config.headers || {}
|
|
250
|
+
config.headers.Authorization = `Bearer ${userStore.getUserAccessToken}`
|
|
251
|
+
userStore.updateLastActiveTime?.()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return config
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
axios.interceptors.response.use(
|
|
258
|
+
res => res,
|
|
259
|
+
error => {
|
|
260
|
+
if (error.response?.status === 401) {
|
|
261
|
+
logoutToAuth()
|
|
262
|
+
}
|
|
263
|
+
return Promise.reject(error)
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
```
|
|
174
267
|
|
|
175
|
-
###
|
|
268
|
+
### 7. 项目视图目录约定
|
|
176
269
|
|
|
177
270
|
`portal-core` 会根据后端菜单返回的 `path` 去匹配前端视图,规则如下:
|
|
178
271
|
|
|
@@ -196,15 +289,16 @@ export default router
|
|
|
196
289
|
- 若本地无 token,会尝试通过 `syncFromAuthStore()` 从统一登录站点恢复;
|
|
197
290
|
- 若仍无 token,则跳转到 `authLoginUrl`,并携带当前地址作为 `redirect`;
|
|
198
291
|
- 若用户已登录但菜单未加载:
|
|
199
|
-
-
|
|
292
|
+
- 会清空之前的动态路由,并通过 `resetMenuLoaded()` 重置菜单状态;
|
|
200
293
|
- 调用 `fetchUserMenu()` 拉取菜单,成功后:
|
|
201
294
|
- 按菜单生成动态路由并挂到 `main` 下;
|
|
202
|
-
-
|
|
295
|
+
- 若当前访问的是根路径 `/`,会根据 `getFirstPage(allMenu)` 计算首页路径并重定向过去;
|
|
296
|
+
- 若当前访问的是具体业务路径(例如刷新了某个子页面),则保持在当前路径,不强制跳转,Tab 也不会被清空;
|
|
203
297
|
- 若菜单已加载且访问根路径 `/`:
|
|
204
|
-
-
|
|
298
|
+
- 仅将 `/` 映射到 `getFirstPage(allMenu)` 对应的页面,不再清空 Tab。
|
|
205
299
|
|
|
206
300
|
这样可以保证:
|
|
207
301
|
|
|
208
302
|
- 登录后总是落在第一个业务页面;
|
|
209
|
-
-
|
|
303
|
+
- 刷新页面或重新打开浏览器时,能自动恢复登录并重新加载菜单,同时尽量停留在当前业务页面和已有 Tab 上;
|
|
210
304
|
- 不同子系统之间互不干扰,只共享统一登录站点。
|
package/dist/index.js
CHANGED
|
@@ -98,8 +98,6 @@ function createAppRouter(options) {
|
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
if (userStore.isMenuLoaded && (to.path === "/" || to.path === "")) {
|
|
101
|
-
const tabStore = getTabStore();
|
|
102
|
-
tabStore.clearAll();
|
|
103
101
|
const allMenu = userStore.getUserALLMenu;
|
|
104
102
|
if (allMenu && allMenu.length > 0) {
|
|
105
103
|
const firstPage = getFirstPage(allMenu);
|
|
@@ -119,8 +117,6 @@ function createAppRouter(options) {
|
|
|
119
117
|
}
|
|
120
118
|
if (!userStore.isMenuLoaded) {
|
|
121
119
|
clearDynamicRoutes();
|
|
122
|
-
const tabStore = getTabStore();
|
|
123
|
-
tabStore.clearAll();
|
|
124
120
|
userStore.resetMenuLoaded();
|
|
125
121
|
const success = await userStore.fetchUserMenu();
|
|
126
122
|
if (!success) {
|
|
@@ -130,19 +126,21 @@ function createAppRouter(options) {
|
|
|
130
126
|
const allMenu = userStore.getUserALLMenu;
|
|
131
127
|
if (allMenu && allMenu.length > 0) {
|
|
132
128
|
addDynamicRoutes(allMenu);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
queryString
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
if (to.path === "/" || to.path === "") {
|
|
130
|
+
const firstPage = getFirstPage(allMenu);
|
|
131
|
+
if (firstPage) {
|
|
132
|
+
const [basePath, queryString] = firstPage.split("?");
|
|
133
|
+
const query = {};
|
|
134
|
+
if (queryString) {
|
|
135
|
+
queryString.split("&").forEach((param) => {
|
|
136
|
+
const [key, value] = param.split("=");
|
|
137
|
+
if (key) query[key] = value || "";
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
justLoadedMenu = true;
|
|
141
|
+
next({ path: basePath, query, replace: true });
|
|
142
|
+
return;
|
|
142
143
|
}
|
|
143
|
-
justLoadedMenu = true;
|
|
144
|
-
next({ path: basePath, query, replace: true });
|
|
145
|
-
return;
|
|
146
144
|
}
|
|
147
145
|
}
|
|
148
146
|
}
|