@litianxiang/portal-core 0.1.8 → 0.1.9

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 CHANGED
@@ -1,11 +1,12 @@
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
+ - http 请求拦截(包含全局 Loading、Token 过期刷新、401 统一退出等)
9
10
 
10
11
  各子系统只需注入自己的静态路由、用户 Store、(可选)Tab Store 和“第一个页面”的计算函数,即可接入。
11
12
 
@@ -32,7 +33,8 @@ npm install @litianxiang/portal-core
32
33
  import {
33
34
  createAppRouter,
34
35
  createLogoutToAuth,
35
- calcInactivityAction
36
+ calcInactivityAction,
37
+ createAuthHttpClient
36
38
  } from '@litianxiang/portal-core'
37
39
  ```
38
40
 
@@ -80,6 +82,29 @@ function calcInactivityAction(
80
82
  bufferMs: number
81
83
  }
82
84
  ```
85
+
86
+ `http` 相关工具简化说明:
87
+
88
+ ```ts
89
+ interface CreateAuthHttpClientOptions {
90
+ axios: any
91
+ baseURL?: string
92
+ timeout?: number
93
+ getUserStore: () => any
94
+ getLoadingStore: () => any
95
+ authLoginUrl?: string
96
+ ssoStorageKey?: string
97
+ refreshUrl: string
98
+ inactiveExpireMs?: number
99
+ inactiveBufferMs?: number
100
+ onShowError?: (msg: string) => void
101
+ }
102
+
103
+ function createAuthHttpClient(options: CreateAuthHttpClientOptions): {
104
+ http: any // 已挂好请求/响应拦截器的 axios 实例
105
+ logoutToAuth: () => void // 同 createLogoutToAuth,便于复用
106
+ }
107
+ ```
83
108
  ```
84
109
 
85
110
  ## 子系统接入步骤
@@ -159,19 +184,19 @@ export const useUserStore = defineStore('user', {
159
184
 
160
185
  ### 3. 准备 Tab Store(可选)
161
186
 
162
- Tab Store 主要用于和各系统的 Tab 控件协作,例如在“手动退出”时统一清理 Tab。当前路由内核在菜单加载 / 刷新场景下不会自动清空 Tab。
187
+ Tab Store 主要用于和各系统的 Tab 控件协作,例如在“手动退出”时统一关闭所有可关闭的 Tab。当前路由内核在菜单加载 / 刷新场景下不会自动清空 Tab。
163
188
 
164
189
  建议至少包含:
165
190
 
166
- - `clearAll()`:清空当前系统的所有 Tab
191
+ - `closeAll()`:关闭所有可关闭的 Tab(保留固定首页等不可关闭项)
167
192
 
168
193
  示例:
169
194
 
170
195
  ```ts
171
196
  export const useTabStore = defineStore('tab', {
172
197
  actions: {
173
- clearAll() {
174
- // 清空当前系统的所有 Tab
198
+ closeAll() {
199
+ // 关闭所有可关闭的 Tab(保留固定首页等不可关闭项)
175
200
  }
176
201
  }
177
202
  })
@@ -211,60 +236,53 @@ const router = createAppRouter({
211
236
 
212
237
  export default router
213
238
  ```
214
- ### 6.(可选)接入统一退出与空闲超时控制
239
+ ### 6. 使用 createAuthHttpClient 统一封装 http(推荐)
240
+
241
+ 在各子系统的 http 封装中,推荐直接使用 `createAuthHttpClient`,即可一次性接入:
242
+
243
+ - 全局 Loading 控制(基于 `getLoadingStore` 的 `startLoading/stopLoading`)
244
+ - 基于最后操作时间的“2 小时未操作自动退出”
245
+ - 距离过期 5 分钟以内自动刷新 access_token
246
+ - 401 统一退出到认证站并弹出错误提示
215
247
 
216
- 在各子系统的 http 封装中,可以结合 `createLogoutToAuth` 和 `calcInactivityAction` 实现统一退出与“2 小时未操作自动退出”的逻辑。例如:
248
+ 示例:
217
249
 
218
250
  ```ts
219
251
  // src/utils/http.ts
220
252
  import axios from 'axios'
221
- import { createLogoutToAuth, calcInactivityAction } from '@litianxiang/portal-core'
253
+ import { ElMessage } from 'element-plus'
254
+ import { createAuthHttpClient } from '@litianxiang/portal-core'
222
255
  import { useUserStore } from '@/stores/user'
223
256
  import { useLoadingStore } from '@/stores/loading'
224
257
 
225
- const logoutToAuth = createLogoutToAuth({
258
+ const AUTH_LOGIN_URL =
259
+ import.meta.env.VITE_AUTH_LOGIN_URL || `${window.location.origin}/auth/#/login`
260
+
261
+ const { http, logoutToAuth } = createAuthHttpClient({
262
+ axios,
263
+ baseURL: '',
264
+ timeout: 10000,
226
265
  getUserStore: () => useUserStore(),
227
266
  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?.()
267
+ authLoginUrl: AUTH_LOGIN_URL,
268
+ ssoStorageKey: 'user-store',
269
+ refreshUrl: '/proxy/auth/api/token/refresh',
270
+ inactiveExpireMs: 2 * 60 * 60 * 1000,
271
+ inactiveBufferMs: 5 * 60 * 1000,
272
+ onShowError: (msg: string) => {
273
+ ElMessage.error(msg)
252
274
  }
253
-
254
- return config
255
275
  })
256
276
 
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
- )
277
+ // 业务代码直接使用 http 即可
278
+ // http.post('/api/demo', data)
279
+
280
+ export default http
281
+ export { logoutToAuth }
266
282
  ```
267
283
 
284
+ 如需完全自定义 http 行为,也可以直接基于前面的 `auth` 工具自行编写拦截器。
285
+
268
286
  ### 7. 项目视图目录约定
269
287
 
270
288
  `portal-core` 会根据后端菜单返回的 `path` 去匹配前端视图,规则如下:
package/dist/index.d.ts CHANGED
@@ -50,4 +50,22 @@ interface InactivityResult {
50
50
  */
51
51
  declare function calcInactivityAction(lastActiveTime: number | null | undefined, now: number, config?: InactivityConfig): InactivityResult;
52
52
 
53
- export { type AppRouterOptions, type InactivityConfig, type InactivityResult, type LogoutToAuthOptions, calcInactivityAction, createAppRouter, createLogoutToAuth };
53
+ interface CreateAuthHttpClientOptions {
54
+ axios: any;
55
+ baseURL?: string;
56
+ timeout?: number;
57
+ getUserStore: () => any;
58
+ getLoadingStore: () => any;
59
+ authLoginUrl?: string;
60
+ ssoStorageKey?: string;
61
+ refreshUrl: string;
62
+ inactiveExpireMs?: number;
63
+ inactiveBufferMs?: number;
64
+ onShowError?: (msg: string) => void;
65
+ }
66
+ declare function createAuthHttpClient(options: CreateAuthHttpClientOptions): {
67
+ http: any;
68
+ logoutToAuth: () => void;
69
+ };
70
+
71
+ export { type AppRouterOptions, type CreateAuthHttpClientOptions, type InactivityConfig, type InactivityResult, type LogoutToAuthOptions, calcInactivityAction, createAppRouter, createAuthHttpClient, createLogoutToAuth };
package/dist/index.js CHANGED
@@ -222,8 +222,157 @@ function calcInactivityAction(lastActiveTime, now, config = {}) {
222
222
  bufferMs
223
223
  };
224
224
  }
225
+
226
+ // src/http.ts
227
+ function createAuthHttpClient(options) {
228
+ const {
229
+ axios,
230
+ baseURL = "",
231
+ timeout = 1e4,
232
+ getUserStore,
233
+ getLoadingStore,
234
+ authLoginUrl,
235
+ ssoStorageKey = "user-store",
236
+ refreshUrl,
237
+ inactiveExpireMs,
238
+ inactiveBufferMs,
239
+ onShowError
240
+ } = options;
241
+ const http = axios.create({
242
+ baseURL,
243
+ timeout
244
+ });
245
+ const logoutToAuth = createLogoutToAuth({
246
+ getUserStore,
247
+ getLoadingStore,
248
+ authLoginUrl,
249
+ ssoStorageKey
250
+ });
251
+ let isRefreshing = false;
252
+ let isHandling401 = false;
253
+ http.interceptors.request.use(async (config) => {
254
+ const loadingStore = getLoadingStore();
255
+ loadingStore?.startLoading?.();
256
+ const userStore = getUserStore();
257
+ const accessToken = userStore.getUserAccessToken;
258
+ const refreshToken = userStore.getUserRefreshToken;
259
+ if (!accessToken || !refreshToken) {
260
+ return config;
261
+ }
262
+ const lastActiveTime = userStore.getUserLastActiveTime ?? 0;
263
+ const currentTime = Date.now();
264
+ const { shouldLogout, shouldRefresh } = calcInactivityAction(lastActiveTime, currentTime, {
265
+ expireMs: inactiveExpireMs,
266
+ bufferMs: inactiveBufferMs
267
+ });
268
+ if (shouldLogout) {
269
+ logoutToAuth();
270
+ return Promise.reject(new Error("INACTIVE_LOGOUT"));
271
+ }
272
+ if (!lastActiveTime) {
273
+ userStore.setUserLastActiveTime?.(currentTime);
274
+ } else {
275
+ if (shouldRefresh && !isRefreshing) {
276
+ isRefreshing = true;
277
+ try {
278
+ const response = await axios.post(refreshUrl, {}, {
279
+ headers: { Authorization: `Bearer ${refreshToken}` }
280
+ });
281
+ const result = response.data;
282
+ if (result.code === 200 && result.data?.access_token) {
283
+ const newAccessToken = result.data.access_token;
284
+ config.headers = config.headers || {};
285
+ config.headers.Authorization = `Bearer ${newAccessToken}`;
286
+ userStore.setUserAccessToken?.(newAccessToken);
287
+ userStore.setUserLastActiveTime?.(Date.now());
288
+ }
289
+ } catch (error) {
290
+ logoutToAuth();
291
+ return Promise.reject(error);
292
+ } finally {
293
+ isRefreshing = false;
294
+ }
295
+ }
296
+ }
297
+ config.headers = config.headers || {};
298
+ config.headers.Authorization = `Bearer ${accessToken}`;
299
+ userStore.setUserLastActiveTime?.(Date.now());
300
+ return config;
301
+ });
302
+ function buildErrorMessage(status, message, statusText) {
303
+ let msg = "";
304
+ switch (status) {
305
+ case 400:
306
+ msg = "\u8BF7\u6C42\u53C2\u6570\u9519\u8BEF";
307
+ break;
308
+ case 401:
309
+ msg = "\u672A\u6388\u6743\uFF0C\u8BF7\u767B\u5F55";
310
+ break;
311
+ case 403:
312
+ msg = "\u65E0\u6743\u9650\u8BBF\u95EE\u6216\u670D\u52A1\u6682\u4E0D\u53EF\u7528";
313
+ break;
314
+ case 404:
315
+ msg = "\u8BF7\u6C42\u5730\u5740\u51FA\u9519";
316
+ break;
317
+ case 408:
318
+ msg = "\u8BF7\u6C42\u8D85\u65F6";
319
+ break;
320
+ case 500:
321
+ msg = "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF";
322
+ break;
323
+ case 501:
324
+ msg = "\u670D\u52A1\u672A\u5B9E\u73B0";
325
+ break;
326
+ case 502:
327
+ msg = "\u7F51\u5173\u9519\u8BEF";
328
+ break;
329
+ case 503:
330
+ msg = "\u670D\u52A1\u4E0D\u53EF\u7528";
331
+ break;
332
+ case 504:
333
+ msg = "\u7F51\u5173\u8D85\u65F6";
334
+ break;
335
+ case 505:
336
+ msg = "HTTP\u7248\u672C\u4E0D\u53D7\u652F\u6301";
337
+ break;
338
+ default:
339
+ msg = "\u8BF7\u6C42\u9519\u8BEF";
340
+ break;
341
+ }
342
+ const baseMsg = message || msg;
343
+ if (!status) return baseMsg;
344
+ return `\u8BF7\u6C42\u72B6\u6001\uFF1A${status}\uFF0C${baseMsg}\u3002\u9519\u8BEF\u4FE1\u606F\uFF1A${statusText || ""}`;
345
+ }
346
+ http.interceptors.response.use(
347
+ (response) => {
348
+ const loadingStore = getLoadingStore();
349
+ loadingStore?.stopLoading?.();
350
+ return response.data;
351
+ },
352
+ (error) => {
353
+ const loadingStore = getLoadingStore();
354
+ loadingStore?.stopLoading?.();
355
+ const status = error.response?.status;
356
+ const rawMsg = error.response?.data?.message;
357
+ const statusText = error.response?.statusText;
358
+ const fullMsg = buildErrorMessage(status, rawMsg, statusText);
359
+ if (status === 401) {
360
+ if (!isHandling401) {
361
+ isHandling401 = true;
362
+ onShowError?.(fullMsg);
363
+ logoutToAuth();
364
+ }
365
+ return Promise.reject(error);
366
+ }
367
+ onShowError?.(fullMsg);
368
+ return Promise.reject(error);
369
+ }
370
+ );
371
+ return { http, logoutToAuth };
372
+ }
225
373
  export {
226
374
  calcInactivityAction,
227
375
  createAppRouter,
376
+ createAuthHttpClient,
228
377
  createLogoutToAuth
229
378
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litianxiang/portal-core",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",