@litianxiang/portal-core 0.1.21 → 0.1.23
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 +312 -269
- package/dist/index.d.ts +191 -8
- package/dist/index.js +518 -198
- package/package.json +17 -3
- package/dist/index.cjs +0 -183
package/README.md
CHANGED
|
@@ -1,373 +1,416 @@
|
|
|
1
|
-
# @litianxiang/portal-core
|
|
1
|
+
# @litianxiang/portal-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
门户子系统通用内核,统一提供:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
-
|
|
19
|
-
# 或
|
|
20
|
-
npm install @litianxiang/portal-core
|
|
16
|
+
npm i @litianxiang/portal-core
|
|
21
17
|
```
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
## 快速开始
|
|
24
20
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
### 2) HTTP 接入
|
|
43
39
|
|
|
44
40
|
```ts
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
```
|
|
53
|
+
|
|
54
|
+
## 导出方法说明(逐方法)
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
### app
|
|
54
57
|
|
|
55
|
-
`
|
|
58
|
+
- `setupPortalApp(options)`:统一执行应用启动编排(插件注册、图标注册、beforeMount、mount)。
|
|
59
|
+
|
|
60
|
+
最小示例:
|
|
56
61
|
|
|
57
62
|
```ts
|
|
58
|
-
|
|
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
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
setupPortalApp({
|
|
66
|
+
app,
|
|
67
|
+
plugins: [router, pinia],
|
|
68
|
+
elementIcons: ElementPlusIconsVue,
|
|
69
|
+
mountSelector: '#app'
|
|
70
|
+
})
|
|
71
|
+
```
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
interface InactivityOptions {
|
|
70
|
-
expireMs?: number // token 允许的最大空闲时间,默认 2 小时
|
|
71
|
-
bufferMs?: number // 提前刷新 token 的缓冲时间,默认 5 分钟
|
|
72
|
-
}
|
|
73
|
+
### router
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
- `createAppRouter(options)`:创建统一路由实例,内置登录态校验、菜单动态挂载、首屏跳转逻辑。
|
|
76
|
+
|
|
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
|
-
|
|
89
|
+
### auth
|
|
90
|
+
|
|
91
|
+
- `createLogoutToAuth(options)`:生成统一退出函数,负责清理用户态并跳转到认证站。
|
|
92
|
+
- `calcInactivityAction(lastActiveTime, now, config)`:根据空闲时长计算是否应刷新 token 或强制退出。
|
|
93
|
+
|
|
94
|
+
最小示例:
|
|
88
95
|
|
|
89
96
|
```ts
|
|
90
|
-
|
|
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'
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
const logoutToAuth = createLogoutToAuth({
|
|
100
|
+
getUserStore: () => useUserStore(),
|
|
101
|
+
getLoadingStore: () => useLoadingStore()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const { shouldLogout, shouldRefresh } = calcInactivityAction(lastActiveTime, Date.now())
|
|
105
|
+
if (shouldLogout) logoutToAuth()
|
|
106
|
+
if (shouldRefresh) {
|
|
107
|
+
// 执行刷新 token
|
|
107
108
|
}
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
### http
|
|
112
|
+
|
|
113
|
+
- `createAuthHttpClient(options)`:创建带认证拦截器的 HTTP 客户端(Loading、token 注入、401 处理、刷新 token)。
|
|
114
|
+
|
|
115
|
+
最小示例:
|
|
111
116
|
|
|
112
117
|
```ts
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
getUserStore: () => any
|
|
116
|
-
}
|
|
118
|
+
import axios from 'axios'
|
|
119
|
+
import { createAuthHttpClient } from '@litianxiang/portal-core'
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// 判断是否拥有给定列表中的任意一个权限
|
|
124
|
-
hasAnyPermission: (codesOrPaths: string[]) => boolean
|
|
125
|
-
}
|
|
121
|
+
const { http } = createAuthHttpClient({
|
|
122
|
+
axios,
|
|
123
|
+
getUserStore: () => useUserStore(),
|
|
124
|
+
getLoadingStore: () => useLoadingStore()
|
|
125
|
+
})
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
await http.post('/api/demo', {})
|
|
128
128
|
```
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## 子系统接入步骤
|
|
132
129
|
|
|
133
|
-
|
|
130
|
+
### config
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
- `getDefaultAuthLoginUrl()`:基于当前域名生成默认统一登录地址。
|
|
136
133
|
|
|
137
|
-
|
|
134
|
+
最小示例:
|
|
138
135
|
|
|
139
136
|
```ts
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
]
|
|
137
|
+
import { getDefaultAuthLoginUrl } from '@litianxiang/portal-core'
|
|
138
|
+
|
|
139
|
+
const authLoginUrl = getDefaultAuthLoginUrl()
|
|
157
140
|
```
|
|
158
141
|
|
|
159
|
-
|
|
142
|
+
### permission
|
|
160
143
|
|
|
161
|
-
- `
|
|
144
|
+
- `createPermissionHelper(options)`:创建权限判断工具(页面权限、按钮权限、任一权限命中)。
|
|
162
145
|
|
|
163
|
-
|
|
146
|
+
最小示例:
|
|
164
147
|
|
|
165
|
-
|
|
148
|
+
```ts
|
|
149
|
+
import { createPermissionHelper } from '@litianxiang/portal-core'
|
|
150
|
+
|
|
151
|
+
const permission = createPermissionHelper({ getUserStore: () => useUserStore() })
|
|
152
|
+
const canVisit = permission.hasPagePermission('/system/user')
|
|
153
|
+
const canCreate = permission.hasButtonPermission('/system/user:add')
|
|
154
|
+
```
|
|
166
155
|
|
|
167
|
-
|
|
168
|
-
- `syncFromAuthStore()`:从统一登录站点恢复登录态,返回 boolean
|
|
169
|
-
- `isMenuLoaded`:菜单是否已加载(boolean)
|
|
170
|
-
- `getUserALLMenu`:拍平后的菜单数组(用于 `getFirstPage`、挂载动态路由)
|
|
171
|
-
- `resetMenuLoaded()`:重置菜单加载状态
|
|
172
|
-
- `fetchUserMenu()`:拉取菜单并填充 `getUserALLMenu`、`isMenuLoaded`,返回 boolean 是否成功
|
|
156
|
+
### menu
|
|
173
157
|
|
|
174
|
-
|
|
158
|
+
- `createMenuHelper(options)`:创建菜单辅助工具,提供路径查找、标题、父链、面包屑能力。
|
|
159
|
+
- `transformMenuTree(menuList, options)`:将后端菜单转换为标准 `CoreMenuItem` 结构。
|
|
160
|
+
- `buildAllMenuTree(menuList)`:构建完整菜单树(含页面与按钮)。
|
|
161
|
+
- `buildPageMenuList(menuList)`:从完整树裁剪 page/button 结构。
|
|
162
|
+
- `findFirstPagePath(menuList)`:递归查找第一个可访问页面路径。
|
|
163
|
+
|
|
164
|
+
最小示例:
|
|
175
165
|
|
|
176
166
|
```ts
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
```
|
|
167
|
+
import {
|
|
168
|
+
createMenuHelper,
|
|
169
|
+
transformMenuTree,
|
|
170
|
+
buildAllMenuTree,
|
|
171
|
+
buildPageMenuList,
|
|
172
|
+
findFirstPagePath
|
|
173
|
+
} from '@litianxiang/portal-core'
|
|
205
174
|
|
|
206
|
-
|
|
175
|
+
const tree = transformMenuTree(rawMenu)
|
|
176
|
+
const allMenu = buildAllMenuTree(tree)
|
|
177
|
+
const pageMenu = buildPageMenuList(allMenu)
|
|
178
|
+
const firstPath = findFirstPagePath(allMenu)
|
|
207
179
|
|
|
208
|
-
|
|
180
|
+
const helper = createMenuHelper({ getMenuTree: () => allMenu })
|
|
181
|
+
const breadcrumb = helper.getBreadcrumb('/system/user')
|
|
182
|
+
```
|
|
209
183
|
|
|
210
|
-
|
|
184
|
+
### user
|
|
211
185
|
|
|
212
|
-
- `
|
|
186
|
+
- `createBaseUserState()`:创建基础用户状态(不含菜单)。
|
|
187
|
+
- `createBaseUserGetters()`:创建基础用户 getters(工号/姓名/token/最后活跃时间)。
|
|
188
|
+
- `createBaseUserActions(options?)`:创建基础用户 actions(set/clear/syncFromAuthStore)。
|
|
189
|
+
- `createMenuUserState()`:创建管理站点用户状态(含 all_menu/sidebar_menu/menuLoaded)。
|
|
190
|
+
- `createMenuUserGetters()`:创建管理站点用户 getters(含菜单相关 getters)。
|
|
191
|
+
- `createMenuUserActions(options)`:创建管理站点 actions(菜单加载、SSO 恢复、状态维护)。
|
|
192
|
+
- `filterSidebarMenu(menus, options?)`:递归过滤不应在侧边栏展示的菜单类别。
|
|
193
|
+
- `syncFromAuthStoreToStore(store, options?)`:从统一登录缓存恢复用户信息到 store。
|
|
194
|
+
- `fetchUserMenuForStore(store, options)`:请求后端菜单并写入 `all_menu/sidebar_menu`。
|
|
213
195
|
|
|
214
|
-
|
|
196
|
+
最小示例:
|
|
215
197
|
|
|
216
198
|
```ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
199
|
+
import {
|
|
200
|
+
createBaseUserState,
|
|
201
|
+
createBaseUserGetters,
|
|
202
|
+
createBaseUserActions,
|
|
203
|
+
createMenuUserState,
|
|
204
|
+
createMenuUserGetters,
|
|
205
|
+
createMenuUserActions,
|
|
206
|
+
filterSidebarMenu,
|
|
207
|
+
syncFromAuthStoreToStore,
|
|
208
|
+
fetchUserMenuForStore
|
|
209
|
+
} from '@litianxiang/portal-core'
|
|
210
|
+
|
|
211
|
+
// 轻量站点
|
|
212
|
+
const baseState = createBaseUserState()
|
|
213
|
+
const baseGetters = createBaseUserGetters()
|
|
214
|
+
const baseActions = createBaseUserActions()
|
|
215
|
+
|
|
216
|
+
// 管理站点
|
|
217
|
+
const menuState = createMenuUserState()
|
|
218
|
+
const menuGetters = createMenuUserGetters()
|
|
219
|
+
const menuActions = createMenuUserActions({ http, site: 'manage', transformMenuData, getALLMenu })
|
|
220
|
+
|
|
221
|
+
const restored = syncFromAuthStoreToStore(menuState)
|
|
222
|
+
const sidebar = filterSidebarMenu(menuState.all_menu)
|
|
223
|
+
await fetchUserMenuForStore(menuState, { http, site: 'manage', transformMenuData, getALLMenu })
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
###
|
|
226
|
+
### tab
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
- `addTab(tabs, tab)`:新增标签页(同路径不重复)。
|
|
229
|
+
- `removeTab(tabs, path)`:按路径删除标签页。
|
|
230
|
+
- `closeOthers(tabs, path, homePath?)`:关闭除当前与首页外的标签页。
|
|
231
|
+
- `closeLeft(tabs, path, homePath?)`:关闭当前左侧标签页(首页保留)。
|
|
232
|
+
- `closeRight(tabs, path, homePath?)`:关闭当前右侧标签页(首页保留)。
|
|
233
|
+
- `closeAll(tabs)`:关闭全部可关闭标签页。
|
|
234
|
+
- `createTabStoreOptions(homePath?)`:生成可直接用于 Pinia 的 Tab Store 配置。
|
|
235
|
+
|
|
236
|
+
最小示例:
|
|
229
237
|
|
|
230
238
|
```ts
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
import {
|
|
240
|
+
addTab,
|
|
241
|
+
removeTab,
|
|
242
|
+
closeOthers,
|
|
243
|
+
closeLeft,
|
|
244
|
+
closeRight,
|
|
245
|
+
closeAll,
|
|
246
|
+
createTabStoreOptions
|
|
247
|
+
} from '@litianxiang/portal-core'
|
|
248
|
+
|
|
249
|
+
let tabs = addTab([], { title: '首页', path: '/main/home', closable: false, keepAlive: true })
|
|
250
|
+
tabs = addTab(tabs, { title: '用户管理', path: '/system/user', closable: true, keepAlive: true })
|
|
251
|
+
tabs = closeOthers(tabs, '/system/user')
|
|
252
|
+
tabs = closeLeft(tabs, '/system/user')
|
|
253
|
+
tabs = closeRight(tabs, '/system/user')
|
|
254
|
+
tabs = removeTab(tabs, '/system/user')
|
|
255
|
+
tabs = closeAll(tabs)
|
|
256
|
+
|
|
257
|
+
const tabStoreOptions = createTabStoreOptions('/main/home')
|
|
236
258
|
```
|
|
237
259
|
|
|
238
|
-
|
|
260
|
+
### loading
|
|
239
261
|
|
|
240
|
-
|
|
262
|
+
- `createLoadingStoreOptions()`:生成可直接用于 Pinia 的 Loading Store 配置。
|
|
263
|
+
|
|
264
|
+
最小示例:
|
|
241
265
|
|
|
242
266
|
```ts
|
|
243
|
-
|
|
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'
|
|
267
|
+
import { createLoadingStoreOptions } from '@litianxiang/portal-core'
|
|
249
268
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
getUserStore: () => useUserStore(),
|
|
253
|
-
getTabStore: () => useTabStore(),
|
|
254
|
-
getFirstPage,
|
|
255
|
-
authLoginUrl: import.meta.env.VITE_AUTH_LOGIN_URL // 可选
|
|
256
|
-
})
|
|
269
|
+
const loadingStoreOptions = createLoadingStoreOptions()
|
|
270
|
+
```
|
|
257
271
|
|
|
258
|
-
|
|
272
|
+
### time
|
|
273
|
+
|
|
274
|
+
- `formatTime(date, format?)`:按指定格式输出时间字符串。
|
|
275
|
+
- `humanizeTime(date)`:输出相对时间(如“3分钟前”)。
|
|
276
|
+
- `timeDiff(start, end?)`:计算时间差(毫秒)。
|
|
277
|
+
- `formatToMonthDay(date)`:格式化为 `MM-DD`。
|
|
278
|
+
- `createRange(start, end)`:生成有序时间区间(自动处理先后)。
|
|
279
|
+
- `formatRange(start, end, format?)`:格式化时间区间字符串。
|
|
280
|
+
- `getPresetRange(key, now?)`:获取常用预设区间(今日/近7天/本月等)。
|
|
281
|
+
- `isInRange(value, range)`:判断时间是否落在区间内(含边界)。
|
|
282
|
+
- `useTime()`:组合导出以上时间工具。
|
|
283
|
+
|
|
284
|
+
最小示例:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import {
|
|
288
|
+
formatTime,
|
|
289
|
+
humanizeTime,
|
|
290
|
+
timeDiff,
|
|
291
|
+
formatToMonthDay,
|
|
292
|
+
createRange,
|
|
293
|
+
formatRange,
|
|
294
|
+
getPresetRange,
|
|
295
|
+
isInRange,
|
|
296
|
+
useTime
|
|
297
|
+
} from '@litianxiang/portal-core'
|
|
298
|
+
|
|
299
|
+
const t1 = formatTime(new Date())
|
|
300
|
+
const t2 = humanizeTime(Date.now() - 60 * 1000)
|
|
301
|
+
const diff = timeDiff('2026-01-01')
|
|
302
|
+
const md = formatToMonthDay(new Date())
|
|
303
|
+
const range = createRange('2026-01-01', '2026-01-31')
|
|
304
|
+
const text = formatRange('2026-01-01', '2026-01-31')
|
|
305
|
+
const preset = getPresetRange('last7days')
|
|
306
|
+
const inRange = isInRange(new Date(), range)
|
|
307
|
+
const timeUtils = useTime()
|
|
259
308
|
```
|
|
260
|
-
### 6. 使用 createAuthHttpClient 统一封装 http(推荐)
|
|
261
309
|
|
|
262
|
-
|
|
310
|
+
### number
|
|
263
311
|
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
267
|
-
- 401 统一退出到认证站并弹出错误提示
|
|
312
|
+
- `formatToYi(value, decimals?)`:按“亿元”单位格式化数值。
|
|
313
|
+
- `formatToWanShou(value, decimals?)`:按“万手”单位格式化数值。
|
|
314
|
+
- `formatWanYuanToYi(value, decimals?)`:将“万元”转换并格式化为“亿元”。
|
|
268
315
|
|
|
269
|
-
|
|
316
|
+
最小示例:
|
|
270
317
|
|
|
271
318
|
```ts
|
|
272
|
-
|
|
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'
|
|
319
|
+
import { formatToYi, formatToWanShou, formatWanYuanToYi } from '@litianxiang/portal-core'
|
|
278
320
|
|
|
279
|
-
const
|
|
280
|
-
|
|
321
|
+
const a = formatToYi(123000000)
|
|
322
|
+
const b = formatToWanShou(560000)
|
|
323
|
+
const c = formatWanYuanToYi(50000)
|
|
324
|
+
```
|
|
281
325
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
})
|
|
326
|
+
### theme
|
|
327
|
+
|
|
328
|
+
- `initTheme()`:从本地缓存读取主题配置并初始化。
|
|
329
|
+
- `applyTheme(config)`:将主题变量应用到根节点 CSS 变量。
|
|
330
|
+
- `useThemeWatcher(config)`:监听主题变化并自动持久化。
|
|
297
331
|
|
|
298
|
-
|
|
299
|
-
// http.post('/api/demo', data)
|
|
332
|
+
最小示例:
|
|
300
333
|
|
|
301
|
-
|
|
302
|
-
|
|
334
|
+
```ts
|
|
335
|
+
import { initTheme, applyTheme, useThemeWatcher } from '@litianxiang/portal-core'
|
|
336
|
+
|
|
337
|
+
const theme = initTheme()
|
|
338
|
+
applyTheme(theme)
|
|
339
|
+
useThemeWatcher(theme)
|
|
303
340
|
```
|
|
304
341
|
|
|
305
|
-
|
|
342
|
+
### i18n
|
|
306
343
|
|
|
307
|
-
|
|
344
|
+
- `createPortalI18n(options)`:创建并返回统一配置的 `vue-i18n` 实例。
|
|
308
345
|
|
|
309
|
-
|
|
346
|
+
最小示例:
|
|
310
347
|
|
|
311
348
|
```ts
|
|
312
|
-
|
|
313
|
-
import { createPermissionHelper } from '@litianxiang/portal-core'
|
|
314
|
-
import { useUserStore } from '@/stores/user'
|
|
349
|
+
import { createPortalI18n } from '@litianxiang/portal-core'
|
|
315
350
|
|
|
316
|
-
const
|
|
317
|
-
|
|
351
|
+
const i18n = createPortalI18n({
|
|
352
|
+
messages: {
|
|
353
|
+
'zh-CN': { common: { ok: '确定' } },
|
|
354
|
+
'en-US': { common: { ok: 'OK' } }
|
|
355
|
+
}
|
|
318
356
|
})
|
|
357
|
+
```
|
|
319
358
|
|
|
320
|
-
|
|
321
|
-
permission.hasPagePermission('/system/menu')
|
|
359
|
+
## 业务方约定
|
|
322
360
|
|
|
323
|
-
|
|
324
|
-
permission.hasButtonPermission('/system/menu:add')
|
|
361
|
+
### userStore(最小能力)
|
|
325
362
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
363
|
+
- `getUserAccessToken`
|
|
364
|
+
- `getUserRefreshToken`
|
|
365
|
+
- `getUserLastActiveTime`
|
|
366
|
+
- `setUserAccessToken(token)`
|
|
367
|
+
- `setUserRefreshToken(token)`
|
|
368
|
+
- `setUserLastActiveTime(ts)`
|
|
369
|
+
- `syncFromAuthStore()`
|
|
370
|
+
- `clearUserInfo()`
|
|
371
|
+
|
|
372
|
+
管理站点再补充:
|
|
332
373
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
374
|
+
- `isMenuLoaded`
|
|
375
|
+
- `resetMenuLoaded()`
|
|
376
|
+
- `fetchUserMenu()`
|
|
377
|
+
- `getUserALLMenu`
|
|
336
378
|
|
|
337
|
-
###
|
|
379
|
+
### 路由结构约定
|
|
338
380
|
|
|
339
|
-
`
|
|
381
|
+
`createAppRouter` 会把动态菜单路由挂到 `name: 'main'` 的节点下,业务静态路由需提供该节点。
|
|
340
382
|
|
|
341
|
-
|
|
342
|
-
- 若找不到,再从 `/src/views` 根目录查找:`/src/views${path}.vue`
|
|
383
|
+
### 视图解析约定
|
|
343
384
|
|
|
344
|
-
|
|
385
|
+
菜单 path 会按以下优先级解析视图:
|
|
345
386
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
- `/src/views/system/user.vue`
|
|
349
|
-
- 若都不存在,会默认认为在 `/src/views/main/system/user.vue`,方便排查。
|
|
387
|
+
1. `/src/views/main${path}.vue`
|
|
388
|
+
2. `/src/views${path}.vue`
|
|
350
389
|
|
|
351
|
-
|
|
390
|
+
## 常见问题
|
|
352
391
|
|
|
353
|
-
|
|
392
|
+
### 1) 刷新后跳登录
|
|
354
393
|
|
|
355
|
-
|
|
394
|
+
先检查 `syncFromAuthStore()` 是否正确从统一登录缓存恢复 token。
|
|
356
395
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
396
|
+
### 2) 401 循环跳转
|
|
397
|
+
|
|
398
|
+
检查是否重复创建多个 http 实例,或在 401 处理中重复手动跳转。
|
|
399
|
+
|
|
400
|
+
### 3) 菜单加载后白屏
|
|
401
|
+
|
|
402
|
+
检查菜单 path 与实际视图文件是否匹配(含 `/main` 约定)。
|
|
403
|
+
|
|
404
|
+
## 本地开发
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pnpm install
|
|
408
|
+
pnpm run build
|
|
409
|
+
pnpm run pack:check
|
|
410
|
+
```
|
|
368
411
|
|
|
369
|
-
|
|
412
|
+
## 发布流程
|
|
370
413
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
414
|
+
1. 更新版本号(patch 递增)
|
|
415
|
+
2. 执行 `pnpm run pack:check`
|
|
416
|
+
3. 发布:`npm publish --access public --registry=https://registry.npmjs.org/`
|