@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.
Files changed (3) hide show
  1. package/README.md +108 -14
  2. package/dist/index.js +14 -16
  3. 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
- - Tab 状态重置与配合各系统的 Tab Store
8
+ - 统一退出到认证站、空闲超时判断等通用认证逻辑
9
9
 
10
- 各子系统只需注入自己的静态路由、用户 StoreTab 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 { createAppRouter } from '@litianxiang/portal-core'
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
- Tab Store 主要用于在根路径访问时清空 Tab,接口建议包含:
164
+ 建议至少包含:
125
165
 
126
- - `clearAll()`:清空所有 Tab
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
- ### 6. 项目视图目录约定
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
- - 会清空之前的动态路由和 Tab;
292
+ - 会清空之前的动态路由,并通过 `resetMenuLoaded()` 重置菜单状态;
200
293
  - 调用 `fetchUserMenu()` 拉取菜单,成功后:
201
294
  - 按菜单生成动态路由并挂到 `main` 下;
202
- - 根据 `getFirstPage(allMenu)` 计算首页路径并重定向过去;
295
+ - 若当前访问的是根路径 `/`,会根据 `getFirstPage(allMenu)` 计算首页路径并重定向过去;
296
+ - 若当前访问的是具体业务路径(例如刷新了某个子页面),则保持在当前路径,不强制跳转,Tab 也不会被清空;
203
297
  - 若菜单已加载且访问根路径 `/`:
204
- - 会先清空 Tab,再跳转到 `getFirstPage(allMenu)` 对应的页面。
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
- const firstPage = getFirstPage(allMenu);
134
- if (firstPage) {
135
- const [basePath, queryString] = firstPage.split("?");
136
- const query = {};
137
- if (queryString) {
138
- queryString.split("&").forEach((param) => {
139
- const [key, value] = param.split("=");
140
- if (key) query[key] = value || "";
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litianxiang/portal-core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",