@litianxiang/portal-core 0.1.22 → 0.1.24

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,373 +1,424 @@
1
- # @litianxiang/portal-core 使用说明
1
+ # @litianxiang/portal-core
2
2
 
3
- `@litianxiang/portal-core` 是一套门户子系统通用的路由 / 认证 / 请求 内核,负责统一处理:
3
+ 门户子系统通用内核,统一提供:
4
4
 
5
- - 登录态校验 + SSO 恢复
6
- - 菜单加载与动态路由挂载
7
- - 根路径重定向到第一个业务页面
8
- - 统一退出到认证站、空闲超时判断等通用认证逻辑
9
- - http 请求拦截(包含全局 Loading、Token 过期刷新、401 统一退出等)
10
-
11
- 各子系统只需注入自己的静态路由、用户 Store、(可选)Tab Store 和“第一个页面”的计算函数,即可接入。
5
+ - 路由守卫与动态菜单挂载
6
+ - SSO 登录态恢复与统一退出
7
+ - 认证 HTTP 拦截器(Loading、401、自动刷新 token)
8
+ - 菜单/权限/用户状态工具
9
+ - Tab、Loading、时间、数字、主题、i18n 工具
12
10
 
13
11
  ## 安装
14
12
 
15
13
  ```bash
16
14
  pnpm add @litianxiang/portal-core
17
15
  # 或
18
- pnpm update @litianxiang/portal-core
19
- # 或
20
- npm install @litianxiang/portal-core
16
+ npm i @litianxiang/portal-core
21
17
  ```
22
18
 
23
- 要求:
19
+ ## 快速开始
24
20
 
25
- - Vue 3
26
- - vue-router 4(hash 模式)
21
+ ### 1) 路由接入
27
22
 
28
- ## 导出的能力
23
+ ```ts
24
+ import { createAppRouter } from '@litianxiang/portal-core'
25
+ import { staticRoutes } from './routes'
26
+ import { useUserStore } from '@/stores/user'
29
27
 
30
- 当前主要导出以下能力:
28
+ const router = createAppRouter({
29
+ staticRoutes,
30
+ getUserStore: () => useUserStore(),
31
+ getFirstPage: (allMenu) => allMenu.find((i: any) => i.category === 'page')?.path,
32
+ authLoginUrl: import.meta.env.VITE_AUTH_LOGIN_URL
33
+ })
31
34
 
32
- ```ts
33
- import {
34
- createAppRouter,
35
- createLogoutToAuth,
36
- calcInactivityAction,
37
- createAuthHttpClient,
38
- createPermissionHelper
39
- } from '@litianxiang/portal-core'
35
+ export default router
40
36
  ```
41
37
 
42
- `createAppRouter` 类型签名简化说明:
38
+ ### 2) HTTP 接入
43
39
 
44
40
  ```ts
45
- interface AppRouterOptions {
46
- staticRoutes: RouteRecordRaw[] // 各系统自己的静态路由(含 main 布局)
47
- getUserStore: () => any // 返回用户 Store 实例
48
- getTabStore: () => any // 返回 Tab Store 实例
49
- getFirstPage: (allMenu: any[]) => string | null | undefined
50
- authLoginUrl?: string // 统一登录地址(可选,不传则默认 /auth/#/login)
51
- }
41
+ import axios from 'axios'
42
+ import { createAuthHttpClient } from '@litianxiang/portal-core'
43
+ import { useUserStore } from '@/stores/user'
44
+ import { useLoadingStore } from '@/stores/loading'
45
+
46
+ export const { http, logoutToAuth } = createAuthHttpClient({
47
+ axios,
48
+ getUserStore: () => useUserStore(),
49
+ getLoadingStore: () => useLoadingStore(),
50
+ onShowError: (msg) => console.error(msg)
51
+ })
52
+ ```
52
53
 
53
- function createAppRouter(options: AppRouterOptions): Router
54
+ ## 导出方法说明(逐方法)
54
55
 
55
- `auth` 相关工具简化说明:
56
+ ### app
57
+
58
+ - `setupPortalApp(options)`:统一执行应用启动编排(插件注册、图标注册、beforeMount、mount)。
59
+
60
+ 最小示例:
56
61
 
57
62
  ```ts
58
- interface LogoutOptions {
59
- getUserStore: () => any // 返回当前系统的 userStore(需提供 clearUserInfo 等方法)
60
- getLoadingStore?: () => any // 返回 loadingStore(可选,用于全局 loading 结束)
61
- authLoginUrl?: string // 统一登录地址(不传则默认 /auth/#/login)
62
- ssoStorageKey?: string // 统一登录本地缓存 key,默认 'user-store'
63
- }
63
+ import { setupPortalApp } from '@litianxiang/portal-core'
64
+
65
+ setupPortalApp({
66
+ app,
67
+ plugins: [router, pinia],
68
+ elementIcons: ElementPlusIconsVue,
69
+ mountSelector: '#app'
70
+ })
71
+ ```
64
72
 
65
- // 生成一个“退出到统一登录”的函数,一般在 http 拦截器或手动退出按钮中调用
66
- function createLogoutToAuth(options: LogoutOptions): () => void
73
+ ### router
67
74
 
68
- // 计算基于最后一次操作时间的“是否需要退出/刷新 token”等动作
69
- interface InactivityOptions {
70
- expireMs?: number // token 允许的最大空闲时间,默认 2 小时
71
- bufferMs?: number // 提前刷新 token 的缓冲时间,默认 5 分钟
72
- }
75
+ - `createAppRouter(options)`:创建统一路由实例,内置登录态校验、菜单动态挂载、首屏跳转逻辑。
73
76
 
74
- function calcInactivityAction(
75
- lastActiveTime: number | null | undefined,
76
- now: number,
77
- options?: InactivityOptions
78
- ): {
79
- shouldLogout: boolean
80
- shouldRefresh: boolean
81
- inactiveMs: number
82
- expireMs: number
83
- bufferMs: number
84
- }
77
+ 最小示例:
78
+
79
+ ```ts
80
+ import { createAppRouter } from '@litianxiang/portal-core'
81
+
82
+ const router = createAppRouter({
83
+ staticRoutes,
84
+ getUserStore: () => useUserStore(),
85
+ getFirstPage: (allMenu) => allMenu.find((m: any) => m.category === 'page')?.path
86
+ })
85
87
  ```
86
88
 
87
- `http` 相关工具简化说明:
89
+ ### auth
90
+
91
+ - `createLogoutToAuth(options)`:生成统一退出函数,负责清理用户态并跳转到认证站。
92
+ - `calcInactivityAction(lastActiveTime, now, config)`:根据空闲时长计算是否应刷新 token 或强制退出。
93
+
94
+ 最小示例:
88
95
 
89
96
  ```ts
90
- interface CreateAuthHttpClientOptions {
91
- axios: any
92
- baseURL?: string
93
- timeout?: number
94
- getUserStore: () => any
95
- getLoadingStore: () => any
96
- authLoginUrl?: string
97
- ssoStorageKey?: string
98
- refreshUrl: string
99
- inactiveExpireMs?: number
100
- inactiveBufferMs?: number
101
- onShowError?: (msg: string) => void
102
- }
97
+ import { createLogoutToAuth, calcInactivityAction } from '@litianxiang/portal-core'
98
+
99
+ const logoutToAuth = createLogoutToAuth({
100
+ getUserStore: () => useUserStore(),
101
+ getLoadingStore: () => useLoadingStore()
102
+ })
103
103
 
104
- function createAuthHttpClient(options: CreateAuthHttpClientOptions): {
105
- http: any // 已挂好请求/响应拦截器的 axios 实例
106
- logoutToAuth: () => void // 同 createLogoutToAuth,便于复用
104
+ const { shouldLogout, shouldRefresh } = calcInactivityAction(lastActiveTime, Date.now())
105
+ if (shouldLogout) logoutToAuth()
106
+ if (shouldRefresh) {
107
+ // 执行刷新 token
107
108
  }
108
109
  ```
109
110
 
110
- `permission` 相关工具简化说明:
111
+ ### http
112
+
113
+ - `createAuthHttpClient(options)`:创建带认证拦截器的 HTTP 客户端(Loading、token 注入、401 处理、刷新 token)。
114
+ - `createPortalHttpClient(options)`:业务项目 HTTP 初始化便捷封装,减少重复模板代码。
115
+
116
+ 最小示例:
111
117
 
112
118
  ```ts
113
- interface PermissionHelperOptions {
114
- // 返回用户 Store 实例,需至少提供 getUserALLMenu getUserPageMenu
115
- getUserStore: () => any
116
- }
119
+ import axios from 'axios'
120
+ import { createAuthHttpClient, createPortalHttpClient } from '@litianxiang/portal-core'
117
121
 
118
- interface PermissionHelper {
119
- // 判断是否拥有某个页面(路由)权限
120
- hasPagePermission: (path: string) => boolean
121
- // 判断是否拥有某个按钮权限(约定使用菜单 path 作为唯一标识)
122
- hasButtonPermission: (codeOrPath: string) => boolean
123
- // 判断是否拥有给定列表中的任意一个权限
124
- hasAnyPermission: (codesOrPaths: string[]) => boolean
125
- }
122
+ const { http } = createAuthHttpClient({
123
+ axios,
124
+ getUserStore: () => useUserStore(),
125
+ getLoadingStore: () => useLoadingStore()
126
+ })
126
127
 
127
- function createPermissionHelper(options: PermissionHelperOptions): PermissionHelper
128
- ```
129
- ```
128
+ await http.post('/api/demo', {})
130
129
 
131
- ## 子系统接入步骤
130
+ // 业务项目推荐直接使用便捷封装
131
+ const { http: portalHttp } = createPortalHttpClient({
132
+ axios,
133
+ getUserStore: () => useUserStore(),
134
+ getLoadingStore: () => useLoadingStore()
135
+ })
136
+ ```
132
137
 
133
- 以某业务系统为例(如 hr.kq):
138
+ ### config
134
139
 
135
- ### 1. 定义静态路由 staticRoutes
140
+ - `getDefaultAuthLoginUrl()`:基于当前域名生成默认统一登录地址。
136
141
 
137
- 静态路由中至少包含主布局 `main`,后续所有菜单页面都会挂到 `main` 下面:
142
+ 最小示例:
138
143
 
139
144
  ```ts
140
- // src/router/routes.ts
141
- import type { RouteRecordRaw } from 'vue-router'
142
-
143
- export const staticRoutes: RouteRecordRaw[] = [
144
- {
145
- path: '/',
146
- name: 'layout',
147
- component: () => import('@/views/layout/index.vue'),
148
- children: [
149
- {
150
- path: '',
151
- name: 'main',
152
- component: () => import('@/views/main/index.vue')
153
- }
154
- ]
155
- }
156
- ]
145
+ import { getDefaultAuthLoginUrl } from '@litianxiang/portal-core'
146
+
147
+ const authLoginUrl = getDefaultAuthLoginUrl()
157
148
  ```
158
149
 
159
- 注意:
150
+ ### permission
151
+
152
+ - `createPermissionHelper(options)`:创建权限判断工具(页面权限、按钮权限、任一权限命中)。
153
+
154
+ 最小示例:
160
155
 
161
- - `createAppRouter` 会把所有菜单对应的动态路由挂到 `name: 'main'` 下面,请确保静态路由中存在 `name: 'main'`。
156
+ ```ts
157
+ import { createPermissionHelper } from '@litianxiang/portal-core'
162
158
 
163
- ### 2. 准备 Pinia userStore
159
+ const permission = createPermissionHelper({ getUserStore: () => useUserStore() })
160
+ const canVisit = permission.hasPagePermission('/system/user')
161
+ const canCreate = permission.hasButtonPermission('/system/user:add')
162
+ ```
164
163
 
165
- userStore 需要提供以下能力(命名可自定义):
164
+ ### menu
166
165
 
167
- - `getUserAccessToken`:当前用户的访问 token(getter)
168
- - `syncFromAuthStore()`:从统一登录站点恢复登录态,返回 boolean
169
- - `isMenuLoaded`:菜单是否已加载(boolean)
170
- - `getUserALLMenu`:拍平后的菜单数组(用于 `getFirstPage`、挂载动态路由)
171
- - `resetMenuLoaded()`:重置菜单加载状态
172
- - `fetchUserMenu()`:拉取菜单并填充 `getUserALLMenu`、`isMenuLoaded`,返回 boolean 是否成功
166
+ - `createMenuHelper(options)`:创建菜单辅助工具,提供路径查找、标题、父链、面包屑能力。
167
+ - `transformMenuTree(menuList, options)`:将后端菜单转换为标准 `CoreMenuItem` 结构。
168
+ - `buildAllMenuTree(menuList)`:构建完整菜单树(含页面与按钮)。
169
+ - `buildPageMenuList(menuList)`:从完整树裁剪 page/button 结构。
170
+ - `findFirstPagePath(menuList)`:递归查找第一个可访问页面路径。
173
171
 
174
- 示例(仅展示接口形态):
172
+ 最小示例:
175
173
 
176
174
  ```ts
177
- export const useUserStore = defineStore('user', {
178
- state: () => ({
179
- accessToken: '',
180
- menuLoaded: false,
181
- allMenu: []
182
- }),
183
- getters: {
184
- getUserAccessToken: state => state.accessToken,
185
- isMenuLoaded: state => state.menuLoaded,
186
- getUserALLMenu: state => state.allMenu
187
- },
188
- actions: {
189
- syncFromAuthStore() {
190
- // 从统一认证站点恢复 token,成功返回 true
191
- },
192
- resetMenuLoaded() {
193
- this.menuLoaded = false
194
- this.allMenu = []
195
- },
196
- async fetchUserMenu() {
197
- // 1. 请求后端菜单树
198
- // 2. 转成拍平的 allMenu
199
- // 3. this.menuLoaded = true
200
- // 返回 true / false 表示是否成功
201
- }
202
- }
203
- })
204
- ```
175
+ import {
176
+ createMenuHelper,
177
+ transformMenuTree,
178
+ buildAllMenuTree,
179
+ buildPageMenuList,
180
+ findFirstPagePath
181
+ } from '@litianxiang/portal-core'
205
182
 
206
- ### 3. 准备 Tab Store(可选)
183
+ const tree = transformMenuTree(rawMenu)
184
+ const allMenu = buildAllMenuTree(tree)
185
+ const pageMenu = buildPageMenuList(allMenu)
186
+ const firstPath = findFirstPagePath(allMenu)
207
187
 
208
- Tab Store 主要用于和各系统的 Tab 控件协作,例如在“手动退出”时统一关闭所有可关闭的 Tab。当前路由内核在菜单加载 / 刷新场景下不会自动清空 Tab。
188
+ const helper = createMenuHelper({ getMenuTree: () => allMenu })
189
+ const breadcrumb = helper.getBreadcrumb('/system/user')
190
+ ```
209
191
 
210
- 建议至少包含:
192
+ ### user
211
193
 
212
- - `closeAll()`:关闭所有可关闭的 Tab(保留固定首页等不可关闭项)
194
+ - `createBaseUserState()`:创建基础用户状态(不含菜单)。
195
+ - `createBaseUserGetters()`:创建基础用户 getters(工号/姓名/token/最后活跃时间)。
196
+ - `createBaseUserActions(options?)`:创建基础用户 actions(set/clear/syncFromAuthStore)。
197
+ - `createMenuUserState()`:创建管理站点用户状态(含 all_menu/sidebar_menu/menuLoaded)。
198
+ - `createMenuUserGetters()`:创建管理站点用户 getters(含菜单相关 getters)。
199
+ - `createMenuUserActions(options)`:创建管理站点 actions(菜单加载、SSO 恢复、状态维护)。
200
+ - `filterSidebarMenu(menus, options?)`:递归过滤不应在侧边栏展示的菜单类别。
201
+ - `syncFromAuthStoreToStore(store, options?)`:从统一登录缓存恢复用户信息到 store。
202
+ - `fetchUserMenuForStore(store, options)`:请求后端菜单并写入 `all_menu/sidebar_menu`。
213
203
 
214
- 示例:
204
+ 最小示例:
215
205
 
216
206
  ```ts
217
- export const useTabStore = defineStore('tab', {
218
- actions: {
219
- closeAll() {
220
- // 关闭所有可关闭的 Tab(保留固定首页等不可关闭项)
221
- }
222
- }
223
- })
207
+ import {
208
+ createBaseUserState,
209
+ createBaseUserGetters,
210
+ createBaseUserActions,
211
+ createMenuUserState,
212
+ createMenuUserGetters,
213
+ createMenuUserActions,
214
+ filterSidebarMenu,
215
+ syncFromAuthStoreToStore,
216
+ fetchUserMenuForStore
217
+ } from '@litianxiang/portal-core'
218
+
219
+ // 轻量站点
220
+ const baseState = createBaseUserState()
221
+ const baseGetters = createBaseUserGetters()
222
+ const baseActions = createBaseUserActions()
223
+
224
+ // 管理站点
225
+ const menuState = createMenuUserState()
226
+ const menuGetters = createMenuUserGetters()
227
+ const menuActions = createMenuUserActions({ http, site: 'manage', transformMenuData, getALLMenu })
228
+
229
+ const restored = syncFromAuthStoreToStore(menuState)
230
+ const sidebar = filterSidebarMenu(menuState.all_menu)
231
+ await fetchUserMenuForStore(menuState, { http, site: 'manage', transformMenuData, getALLMenu })
224
232
  ```
225
233
 
226
- ### 4. 实现 getFirstPage
234
+ ### tab
235
+
236
+ - `addTab(tabs, tab)`:新增标签页(同路径不重复)。
237
+ - `removeTab(tabs, path)`:按路径删除标签页。
238
+ - `closeOthers(tabs, path, homePath?)`:关闭除当前与首页外的标签页。
239
+ - `closeLeft(tabs, path, homePath?)`:关闭当前左侧标签页(首页保留)。
240
+ - `closeRight(tabs, path, homePath?)`:关闭当前右侧标签页(首页保留)。
241
+ - `closeAll(tabs)`:关闭全部可关闭标签页。
242
+ - `createTabStoreOptions(homePath?)`:生成可直接用于 Pinia 的 Tab Store 配置。
227
243
 
228
- 根据拍平的菜单数组 `allMenu`,返回“第一个业务页面”的完整路径(可带 query):
244
+ 最小示例:
229
245
 
230
246
  ```ts
231
- export function getFirstPage(allMenu: any[]): string | null {
232
- // 例如:返回第一个 category === 'page' 的 path
233
- const item = allMenu.find(m => m.category === 'page')
234
- return item?.path || null
235
- }
247
+ import {
248
+ addTab,
249
+ removeTab,
250
+ closeOthers,
251
+ closeLeft,
252
+ closeRight,
253
+ closeAll,
254
+ createTabStoreOptions
255
+ } from '@litianxiang/portal-core'
256
+
257
+ let tabs = addTab([], { title: '首页', path: '/main/home', closable: false, keepAlive: true })
258
+ tabs = addTab(tabs, { title: '用户管理', path: '/system/user', closable: true, keepAlive: true })
259
+ tabs = closeOthers(tabs, '/system/user')
260
+ tabs = closeLeft(tabs, '/system/user')
261
+ tabs = closeRight(tabs, '/system/user')
262
+ tabs = removeTab(tabs, '/system/user')
263
+ tabs = closeAll(tabs)
264
+
265
+ const tabStoreOptions = createTabStoreOptions('/main/home')
236
266
  ```
237
267
 
238
- > 注意:如果返回值中包含查询参数(例如 `'/kq/leave?type=1'`),`createAppRouter` 会自动帮你拆分 path 和 query。
268
+ ### loading
269
+
270
+ - `createLoadingStoreOptions()`:生成可直接用于 Pinia 的 Loading Store 配置。
239
271
 
240
- ### 5. 在 router/index.ts 中使用 createAppRouter
272
+ 最小示例:
241
273
 
242
274
  ```ts
243
- // src/router/index.ts
244
- import { createAppRouter } from '@litianxiang/portal-core'
245
- import { useUserStore } from '@/stores/user'
246
- import { useTabStore } from '@/stores/tab'
247
- import { staticRoutes } from './routes'
248
- import { getFirstPage } from '@/utils/menu'
275
+ import { createLoadingStoreOptions } from '@litianxiang/portal-core'
249
276
 
250
- const router = createAppRouter({
251
- staticRoutes,
252
- getUserStore: () => useUserStore(),
253
- getTabStore: () => useTabStore(),
254
- getFirstPage,
255
- authLoginUrl: import.meta.env.VITE_AUTH_LOGIN_URL // 可选
256
- })
277
+ const loadingStoreOptions = createLoadingStoreOptions()
278
+ ```
257
279
 
258
- export default router
280
+ ### time
281
+
282
+ - `formatTime(date, format?)`:按指定格式输出时间字符串。
283
+ - `humanizeTime(date)`:输出相对时间(如“3分钟前”)。
284
+ - `timeDiff(start, end?)`:计算时间差(毫秒)。
285
+ - `formatToMonthDay(date)`:格式化为 `MM-DD`。
286
+ - `createRange(start, end)`:生成有序时间区间(自动处理先后)。
287
+ - `formatRange(start, end, format?)`:格式化时间区间字符串。
288
+ - `getPresetRange(key, now?)`:获取常用预设区间(今日/近7天/本月等)。
289
+ - `isInRange(value, range)`:判断时间是否落在区间内(含边界)。
290
+ - `useTime()`:组合导出以上时间工具。
291
+
292
+ 最小示例:
293
+
294
+ ```ts
295
+ import {
296
+ formatTime,
297
+ humanizeTime,
298
+ timeDiff,
299
+ formatToMonthDay,
300
+ createRange,
301
+ formatRange,
302
+ getPresetRange,
303
+ isInRange,
304
+ useTime
305
+ } from '@litianxiang/portal-core'
306
+
307
+ const t1 = formatTime(new Date())
308
+ const t2 = humanizeTime(Date.now() - 60 * 1000)
309
+ const diff = timeDiff('2026-01-01')
310
+ const md = formatToMonthDay(new Date())
311
+ const range = createRange('2026-01-01', '2026-01-31')
312
+ const text = formatRange('2026-01-01', '2026-01-31')
313
+ const preset = getPresetRange('last7days')
314
+ const inRange = isInRange(new Date(), range)
315
+ const timeUtils = useTime()
259
316
  ```
260
- ### 6. 使用 createAuthHttpClient 统一封装 http(推荐)
261
317
 
262
- 在各子系统的 http 封装中,推荐直接使用 `createAuthHttpClient`,即可一次性接入:
318
+ ### number
263
319
 
264
- - 全局 Loading 控制(基于 `getLoadingStore` 的 `startLoading/stopLoading`)
265
- - 基于最后操作时间的“2 小时未操作自动退出”
266
- - 距离过期 5 分钟以内自动刷新 access_token
267
- - 401 统一退出到认证站并弹出错误提示
320
+ - `formatToYi(value, decimals?)`:按“亿元”单位格式化数值。
321
+ - `formatToWanShou(value, decimals?)`:按“万手”单位格式化数值。
322
+ - `formatWanYuanToYi(value, decimals?)`:将“万元”转换并格式化为“亿元”。
268
323
 
269
- 示例:
324
+ 最小示例:
270
325
 
271
326
  ```ts
272
- // src/utils/http.ts
273
- import axios from 'axios'
274
- import { ElMessage } from 'element-plus'
275
- import { createAuthHttpClient } from '@litianxiang/portal-core'
276
- import { useUserStore } from '@/stores/user'
277
- import { useLoadingStore } from '@/stores/loading'
327
+ import { formatToYi, formatToWanShou, formatWanYuanToYi } from '@litianxiang/portal-core'
278
328
 
279
- const AUTH_LOGIN_URL =
280
- import.meta.env.VITE_AUTH_LOGIN_URL || `${window.location.origin}/auth/#/login`
329
+ const a = formatToYi(123000000)
330
+ const b = formatToWanShou(560000)
331
+ const c = formatWanYuanToYi(50000)
332
+ ```
281
333
 
282
- const { http, logoutToAuth } = createAuthHttpClient({
283
- axios,
284
- baseURL: '',
285
- timeout: 10000,
286
- getUserStore: () => useUserStore(),
287
- getLoadingStore: () => useLoadingStore(),
288
- authLoginUrl: AUTH_LOGIN_URL,
289
- ssoStorageKey: 'user-store',
290
- refreshUrl: '/proxy/auth/api/token/refresh',
291
- inactiveExpireMs: 2 * 60 * 60 * 1000,
292
- inactiveBufferMs: 5 * 60 * 1000,
293
- onShowError: (msg: string) => {
294
- ElMessage.error(msg)
295
- }
296
- })
334
+ ### theme
297
335
 
298
- // 业务代码直接使用 http 即可
299
- // http.post('/api/demo', data)
336
+ - `initTheme()`:从本地缓存读取主题配置并初始化。
337
+ - `applyTheme(config)`:将主题变量应用到根节点 CSS 变量。
338
+ - `useThemeWatcher(config)`:监听主题变化并自动持久化。
300
339
 
301
- export default http
302
- export { logoutToAuth }
340
+ 最小示例:
341
+
342
+ ```ts
343
+ import { initTheme, applyTheme, useThemeWatcher } from '@litianxiang/portal-core'
344
+
345
+ const theme = initTheme()
346
+ applyTheme(theme)
347
+ useThemeWatcher(theme)
303
348
  ```
304
349
 
305
- 如需完全自定义 http 行为,也可以直接基于前面的 `auth` 工具自行编写拦截器。
350
+ ### i18n
306
351
 
307
- ### 7.(可选)接入菜单 / 按钮权限判断
352
+ - `createPortalI18n(options)`:创建并返回统一配置的 `vue-i18n` 实例。
308
353
 
309
- 在需要做“是否显示某个按钮”或“是否放行某个页面”时,可以使用 `createPermissionHelper` 来基于菜单数据做权限判断:
354
+ 最小示例:
310
355
 
311
356
  ```ts
312
- // 例如在某个业务系统的全局 helper 中:
313
- import { createPermissionHelper } from '@litianxiang/portal-core'
314
- import { useUserStore } from '@/stores/user'
357
+ import { createPortalI18n } from '@litianxiang/portal-core'
315
358
 
316
- const permission = createPermissionHelper({
317
- getUserStore: () => useUserStore()
359
+ const i18n = createPortalI18n({
360
+ messages: {
361
+ 'zh-CN': { common: { ok: '确定' } },
362
+ 'en-US': { common: { ok: 'OK' } }
363
+ }
318
364
  })
365
+ ```
319
366
 
320
- // 页面/路由级权限判断(基于菜单的 path,忽略 query 部分)
321
- permission.hasPagePermission('/system/menu')
367
+ ## 业务方约定
322
368
 
323
- // 按钮权限判断(约定后台为按钮类型菜单配置唯一的 path 作为权限码)
324
- permission.hasButtonPermission('/system/menu:add')
369
+ ### userStore(最小能力)
325
370
 
326
- // 任一权限命中即通过(可用于复杂场景)
327
- permission.hasAnyPermission([
328
- '/system/menu:add',
329
- '/system/menu:edit'
330
- ])
331
- ```
371
+ - `getUserAccessToken`
372
+ - `getUserRefreshToken`
373
+ - `getUserLastActiveTime`
374
+ - `setUserAccessToken(token)`
375
+ - `setUserRefreshToken(token)`
376
+ - `setUserLastActiveTime(ts)`
377
+ - `syncFromAuthStore()`
378
+ - `clearUserInfo()`
332
379
 
333
- > 说明:
334
- > - `createPermissionHelper` 内部会优先使用 `userStore.getUserPageMenu`(包含 page + button),若不存在则退回 `userStore.getUserALLMenu`;
335
- > - 匹配时仅按基础路径(去掉 `?query`)比较,避免 query 影响权限判断。
380
+ 管理站点再补充:
336
381
 
337
- ### 8. 项目视图目录约定
382
+ - `isMenuLoaded`
383
+ - `resetMenuLoaded()`
384
+ - `fetchUserMenu()`
385
+ - `getUserALLMenu`
338
386
 
339
- `portal-core` 会根据后端菜单返回的 `path` 去匹配前端视图,规则如下:
387
+ ### 路由结构约定
340
388
 
341
- - 优先从 `/src/views/main` 下查找:`/src/views/main${path}.vue`
342
- - 若找不到,再从 `/src/views` 根目录查找:`/src/views${path}.vue`
389
+ `createAppRouter` 会把动态菜单路由挂到 `name: 'main'` 的节点下,业务静态路由需提供该节点。
343
390
 
344
- 例如:
391
+ ### 视图解析约定
345
392
 
346
- - 菜单 path 为 `/system/user`,会尝试匹配:
347
- - `/src/views/main/system/user.vue`
348
- - `/src/views/system/user.vue`
349
- - 若都不存在,会默认认为在 `/src/views/main/system/user.vue`,方便排查。
393
+ 菜单 path 会按以下优先级解析视图:
350
394
 
351
- 请保证你的业务页面按上述规则存放文件。
395
+ 1. `/src/views/main${path}.vue`
396
+ 2. `/src/views${path}.vue`
352
397
 
353
- ## 路由行为概要
398
+ ## 常见问题
354
399
 
355
- 接入后,路由的关键行为如下:
400
+ ### 1) 刷新后跳登录
356
401
 
357
- - 每次路由跳转前:
358
- - 若本地无 token,会尝试通过 `syncFromAuthStore()` 从统一登录站点恢复;
359
- - 若仍无 token,则跳转到 `authLoginUrl`,并携带当前地址作为 `redirect`;
360
- - 若用户已登录但菜单未加载:
361
- - 会清空之前的动态路由,并通过 `resetMenuLoaded()` 重置菜单状态;
362
- - 调用 `fetchUserMenu()` 拉取菜单,成功后:
363
- - 按菜单生成动态路由并挂到 `main` 下;
364
- - 若当前访问的是根路径 `/`,会根据 `getFirstPage(allMenu)` 计算首页路径并重定向过去;
365
- - 若当前访问的是具体业务路径(例如刷新了某个子页面),则保持在当前路径,不强制跳转,Tab 也不会被清空;
366
- - 若菜单已加载且访问根路径 `/`:
367
- - 仅将 `/` 映射到 `getFirstPage(allMenu)` 对应的页面,不再清空 Tab。
402
+ 先检查 `syncFromAuthStore()` 是否正确从统一登录缓存恢复 token。
403
+
404
+ ### 2) 401 循环跳转
405
+
406
+ 检查是否重复创建多个 http 实例,或在 401 处理中重复手动跳转。
407
+
408
+ ### 3) 菜单加载后白屏
409
+
410
+ 检查菜单 path 与实际视图文件是否匹配(含 `/main` 约定)。
411
+
412
+ ## 本地开发
413
+
414
+ ```bash
415
+ pnpm install
416
+ pnpm run build
417
+ pnpm run pack:check
418
+ ```
368
419
 
369
- 这样可以保证:
420
+ ## 发布流程
370
421
 
371
- - 登录后总是落在第一个业务页面;
372
- - 刷新页面或重新打开浏览器时,能自动恢复登录并重新加载菜单,同时尽量停留在当前业务页面和已有 Tab 上;
373
- - 不同子系统之间互不干扰,只共享统一登录站点。
422
+ 1. 更新版本号(patch 递增)
423
+ 2. 执行 `pnpm run pack:check`
424
+ 3. 发布:`npm publish --access public --registry=https://registry.npmjs.org/`
package/dist/index.d.ts CHANGED
@@ -69,6 +69,30 @@ interface CreateAuthHttpClientOptions {
69
69
  inactiveBufferMs?: number;
70
70
  onShowError?: (msg: string) => void;
71
71
  }
72
+ interface CreatePortalHttpClientOptions {
73
+ axios?: any;
74
+ baseURL?: string;
75
+ timeout?: number;
76
+ getUserStore: () => any;
77
+ getLoadingStore: () => any;
78
+ authLoginUrl?: string;
79
+ ssoStorageKey?: string;
80
+ refreshUrl?: string;
81
+ inactiveExpireMs?: number;
82
+ inactiveBufferMs?: number;
83
+ onShowError?: (msg: string) => void;
84
+ }
85
+ /**
86
+ * 创建业务项目通用 HTTP 客户端(createAuthHttpClient 的便捷封装)。
87
+ *
88
+ * 作用:
89
+ * - 统一管理端项目中的标准 HTTP 初始化写法
90
+ * - 复用 portal-core 认证拦截器能力
91
+ */
92
+ declare function createPortalHttpClient(options: CreatePortalHttpClientOptions): {
93
+ http: any;
94
+ logoutToAuth: () => void;
95
+ };
72
96
  declare function createAuthHttpClient(options: CreateAuthHttpClientOptions): {
73
97
  http: any;
74
98
  logoutToAuth: () => void;
@@ -94,6 +118,9 @@ interface PermissionHelper {
94
118
  */
95
119
  hasAnyPermission: (codesOrPaths: string[]) => boolean;
96
120
  }
121
+ /**
122
+ * 创建统一权限判断工具,提供页面权限、按钮权限和任意权限判断。
123
+ */
97
124
  declare function createPermissionHelper(options: PermissionHelperOptions): PermissionHelper;
98
125
 
99
126
  interface MenuHelperOptions {
@@ -126,6 +153,9 @@ interface MenuHelper {
126
153
  */
127
154
  getBreadcrumb: (path: string) => BreadcrumbItem[];
128
155
  }
156
+ /**
157
+ * 创建菜单辅助工具,统一提供查找、标题、链路、父级与面包屑能力。
158
+ */
129
159
  declare function createMenuHelper(options: MenuHelperOptions): MenuHelper;
130
160
  /**
131
161
  * 标准化后的菜单节点结构,供各前端站点复用
@@ -212,6 +242,9 @@ declare function getPresetRange(key: PresetRangeKey, now?: TimeInput): TimeRange
212
242
  * 判断某个时间是否在区间内(含边界)
213
243
  */
214
244
  declare function isInRange(value: TimeInput, range: TimeRange<TimeInput>): boolean;
245
+ /**
246
+ * 组合导出时间工具方法,便于在业务中按对象方式使用。
247
+ */
215
248
  declare function useTime(): {
216
249
  formatTime: typeof formatTime;
217
250
  humanizeTime: typeof humanizeTime;
@@ -262,11 +295,29 @@ interface TabItem {
262
295
  keepAlive: boolean;
263
296
  }
264
297
  declare const DEFAULT_HOME_PATH = "/main/home";
298
+ /**
299
+ * 向标签页列表追加新标签;若路径已存在则保持原列表。
300
+ */
265
301
  declare function addTab(tabs: TabItem[], tab: TabItem): TabItem[];
302
+ /**
303
+ * 根据路径移除指定标签页。
304
+ */
266
305
  declare function removeTab(tabs: TabItem[], path: string): TabItem[];
306
+ /**
307
+ * 关闭除当前与首页外的其他标签页。
308
+ */
267
309
  declare function closeOthers(tabs: TabItem[], path: string, homePath?: string): TabItem[];
310
+ /**
311
+ * 关闭当前标签左侧的标签页(首页始终保留)。
312
+ */
268
313
  declare function closeLeft(tabs: TabItem[], path: string, homePath?: string): TabItem[];
314
+ /**
315
+ * 关闭当前标签右侧的标签页(首页始终保留)。
316
+ */
269
317
  declare function closeRight(tabs: TabItem[], path: string, homePath?: string): TabItem[];
318
+ /**
319
+ * 关闭所有可关闭标签,仅保留不可关闭项。
320
+ */
270
321
  declare function closeAll(tabs: TabItem[]): TabItem[];
271
322
  /**
272
323
  * 为 Pinia 创建通用的 Tab Store 配置
@@ -305,7 +356,14 @@ interface BaseUserStoreLike {
305
356
  refresh_token: string;
306
357
  lastActiveTime: number | null;
307
358
  }
359
+ /**
360
+ * 创建基础用户状态(不含菜单)。
361
+ * 适用于 portal.web、portal.auth.web 等轻量站点。
362
+ */
308
363
  declare function createBaseUserState(): BaseUserStoreLike;
364
+ /**
365
+ * 创建基础用户 getters(不含菜单)。
366
+ */
309
367
  declare function createBaseUserGetters(): {
310
368
  getUserInfo: (state: any) => {
311
369
  userno: any;
@@ -324,6 +382,9 @@ interface CreateBaseUserActionsOptions {
324
382
  /** SSO 缓存 key,默认读取 AUTH_CONFIG.ssoStorageKey */
325
383
  ssoStorageKey?: string;
326
384
  }
385
+ /**
386
+ * 创建基础用户 actions(不含菜单)。
387
+ */
327
388
  declare function createBaseUserActions(options?: CreateBaseUserActionsOptions): {
328
389
  setUserInfo(this: BaseUserStoreLike, userInfo: any): void;
329
390
  setUserAccessToken(this: BaseUserStoreLike, access_token: string): void;
@@ -341,7 +402,14 @@ interface CoreMenuUserState<TMenu = any> extends CoreUserStoreLike {
341
402
  all_menu: TMenu[];
342
403
  sidebar_menu: TMenu[];
343
404
  }
405
+ /**
406
+ * 创建管理站点用户状态(含菜单)。
407
+ * 适用于带动态菜单的后台系统。
408
+ */
344
409
  declare function createMenuUserState<TMenu = any>(): CoreMenuUserState<TMenu>;
410
+ /**
411
+ * 创建管理站点用户 getters(含菜单)。
412
+ */
345
413
  declare function createMenuUserGetters<TUserInfo = any>(): {
346
414
  getUserInfo: (state: any) => TUserInfo;
347
415
  getUserNo: (state: any) => any;
@@ -388,6 +456,9 @@ interface CreateMenuUserActionsOptions {
388
456
  filterOptions?: SidebarMenuFilterOptions;
389
457
  ssoStorageKey?: string;
390
458
  }
459
+ /**
460
+ * 创建管理站点用户 actions(含菜单请求、菜单状态维护、SSO 恢复)。
461
+ */
391
462
  declare function createMenuUserActions(options: CreateMenuUserActionsOptions): {
392
463
  setUserInfo(this: CoreUserStoreLike, userInfo: any): void;
393
464
  setUserAccessToken(this: CoreUserStoreLike, access_token: string): void;
@@ -455,6 +526,13 @@ interface SetupPortalAppOptions {
455
526
  beforeMount?: () => void;
456
527
  mountSelector?: string;
457
528
  }
529
+ /**
530
+ * 按 Portal 约定完成应用启动编排:
531
+ * 1. 注册插件
532
+ * 2. 注册 Element Plus 图标
533
+ * 3. 执行挂载前回调
534
+ * 4. 挂载应用
535
+ */
458
536
  declare function setupPortalApp(options: SetupPortalAppOptions): void;
459
537
 
460
538
  interface CreateI18nOptions {
@@ -473,4 +551,4 @@ interface CreateI18nOptions {
473
551
  */
474
552
  declare function createPortalI18n(options: CreateI18nOptions): vue_i18n.I18n<Record<string, any>, {}, {}, string, false>;
475
553
 
476
- export { AUTH_CONFIG, type AppRouterOptions, type AuthConfig, type BaseUserStoreLike, type BreadcrumbItem, type CoreMenuItem, type CoreMenuUserState, type CoreUserStoreLike, type CreateAuthHttpClientOptions, type CreateBaseUserActionsOptions, type CreateI18nOptions, type CreateMenuUserActionsOptions, DEFAULT_AUTH_LOGIN_ENV_KEY, DEFAULT_AUTH_LOGIN_PATH, DEFAULT_HOME_PATH, DEFAULT_SSO_STORAGE_KEY, type FetchUserMenuForStoreOptions, type FontSize, type InactivityConfig, type InactivityResult, type LoadingState, type LogoutToAuthOptions, type MenuHelper, type MenuHelperOptions, type PermissionHelper, type PermissionHelperOptions, type PresetRangeKey, type SetupPortalAppOptions, type SidebarMenuFilterOptions, type SyncFromAuthStoreOptions, type TabItem, type ThemeConfig, type ThemeMode, type TimeInput, type TimeRange, type TransformMenuOptions, addTab, applyTheme, buildAllMenuTree, buildPageMenuList, calcInactivityAction, closeAll, closeLeft, closeOthers, closeRight, createAppRouter, createAuthHttpClient, createBaseUserActions, createBaseUserGetters, createBaseUserState, createLoadingStoreOptions, createLogoutToAuth, createMenuHelper, createMenuUserActions, createMenuUserGetters, createMenuUserState, createPermissionHelper, createPortalI18n, createRange, createTabStoreOptions, fetchUserMenuForStore, filterSidebarMenu, findFirstPagePath, formatRange, formatTime, formatToMonthDay, formatToWanShou, formatToYi, formatWanYuanToYi, getDefaultAuthLoginUrl, getPresetRange, humanizeTime, initTheme, isInRange, removeTab, setupPortalApp, syncFromAuthStoreToStore, timeDiff, transformMenuTree, useThemeWatcher, useTime };
554
+ export { AUTH_CONFIG, type AppRouterOptions, type AuthConfig, type BaseUserStoreLike, type BreadcrumbItem, type CoreMenuItem, type CoreMenuUserState, type CoreUserStoreLike, type CreateAuthHttpClientOptions, type CreateBaseUserActionsOptions, type CreateI18nOptions, type CreateMenuUserActionsOptions, type CreatePortalHttpClientOptions, DEFAULT_AUTH_LOGIN_ENV_KEY, DEFAULT_AUTH_LOGIN_PATH, DEFAULT_HOME_PATH, DEFAULT_SSO_STORAGE_KEY, type FetchUserMenuForStoreOptions, type FontSize, type InactivityConfig, type InactivityResult, type LoadingState, type LogoutToAuthOptions, type MenuHelper, type MenuHelperOptions, type PermissionHelper, type PermissionHelperOptions, type PresetRangeKey, type SetupPortalAppOptions, type SidebarMenuFilterOptions, type SyncFromAuthStoreOptions, type TabItem, type ThemeConfig, type ThemeMode, type TimeInput, type TimeRange, type TransformMenuOptions, addTab, applyTheme, buildAllMenuTree, buildPageMenuList, calcInactivityAction, closeAll, closeLeft, closeOthers, closeRight, createAppRouter, createAuthHttpClient, createBaseUserActions, createBaseUserGetters, createBaseUserState, createLoadingStoreOptions, createLogoutToAuth, createMenuHelper, createMenuUserActions, createMenuUserGetters, createMenuUserState, createPermissionHelper, createPortalHttpClient, createPortalI18n, createRange, createTabStoreOptions, fetchUserMenuForStore, filterSidebarMenu, findFirstPagePath, formatRange, formatTime, formatToMonthDay, formatToWanShou, formatToYi, formatWanYuanToYi, getDefaultAuthLoginUrl, getPresetRange, humanizeTime, initTheme, isInRange, removeTab, setupPortalApp, syncFromAuthStoreToStore, timeDiff, transformMenuTree, useThemeWatcher, useTime };
package/dist/index.js CHANGED
@@ -458,6 +458,13 @@ function calcInactivityAction(lastActiveTime, now, config = {}) {
458
458
 
459
459
  // src/http/http.ts
460
460
  import axiosLib from "axios";
461
+ function createPortalHttpClient(options) {
462
+ return createAuthHttpClient({
463
+ baseURL: "",
464
+ timeout: 1e4,
465
+ ...options
466
+ });
467
+ }
461
468
  function createAuthHttpClient(options) {
462
469
  const {
463
470
  axios,
@@ -1200,6 +1207,7 @@ export {
1200
1207
  createMenuUserGetters,
1201
1208
  createMenuUserState,
1202
1209
  createPermissionHelper,
1210
+ createPortalHttpClient,
1203
1211
  createPortalI18n,
1204
1212
  createRange,
1205
1213
  createTabStoreOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litianxiang/portal-core",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",