@litianxiang/portal-core
v0.2.6
Published
门户子系统通用内核,统一提供:
Readme
@litianxiang/portal-core
门户子系统通用内核,统一提供:
- 路由守卫与动态菜单挂载
- SSO 登录态恢复与统一退出
- 认证 HTTP 拦截器(Loading、401、自动刷新 token)
- 菜单/权限/用户状态工具
- Tab、Loading、时间、数字、主题、i18n 工具
安装
pnpm add @litianxiang/portal-core
# 或
npm i @litianxiang/portal-core升级说明
0.2.0(Breaking Change)
- 移除
createPortalHttpClient。 - 统一使用
createAuthHttpClient。 - 业务项目原来通过
createPortalHttpClient传入的baseURL: ''、timeout: 10000可以不再显式传递,createAuthHttpClient内部已提供相同默认值。
迁移示例:
import { createAuthHttpClient } from '@litianxiang/portal-core'
const { http, logoutToAuth } = createAuthHttpClient({
getUserStore: () => useUserStore(),
getLoadingStore: () => useLoadingStore(),
onShowError: (msg) => console.error(msg)
})快速开始
1) 路由接入
import { createAppRouter } from '@litianxiang/portal-core'
import { staticRoutes } from './routes'
import { useUserStore } from '@/stores/user'
const router = createAppRouter({
staticRoutes,
getUserStore: () => useUserStore(),
getFirstPage: (allMenu) => allMenu.find((i: any) => i.category === 'page')?.path,
authLoginUrl: import.meta.env.VITE_AUTH_LOGIN_URL
})
export default router2) HTTP 接入
import axios from 'axios'
import { createAuthHttpClient } from '@litianxiang/portal-core'
import { useUserStore } from '@/stores/user'
import { useLoadingStore } from '@/stores/loading'
export const { http, logoutToAuth } = createAuthHttpClient({
axios,
getUserStore: () => useUserStore(),
getLoadingStore: () => useLoadingStore(),
onShowError: (msg) => console.error(msg)
})如果业务项目只需要一个标准站点 http 模块,也可以直接使用更薄的封装:
import { createSiteHttpModule } from '@litianxiang/portal-core'
export const { http, logoutToAuth } = createSiteHttpModule({
getUserStore: () => useUserStore(),
getLoadingStore: () => useLoadingStore(),
onShowError: (msg) => console.error(msg)
})导出方法说明(逐方法)
app
setupApp(options):统一执行应用启动编排(插件注册、图标注册、beforeMount、mount)。setupRuntime(options):统一执行挂载前的运行时初始化(i18n、登录态同步、菜单状态、portal-ui http 绑定)。
最小示例:
import { setupApp } from '@litianxiang/portal-core'
setupApp({
app,
plugins: [router, pinia],
elementIcons: ElementPlusIconsVue,
mountSelector: '#app'
})router
createAppRouter(options):创建统一路由实例,内置登录态校验、菜单动态挂载、首屏跳转逻辑。- 菜单类别为
page的动态路由会挂载到main下。 - 菜单类别为
flow-page的动态路由会挂载为顶级路由(不进入main主框架)。
- 菜单类别为
最小示例:
import { createAppRouter } from '@litianxiang/portal-core'
const router = createAppRouter({
staticRoutes,
getUserStore: () => useUserStore(),
getFirstPage: (allMenu) => allMenu.find((m: any) => m.category === 'page')?.path
})createStaticAppRouter(options):创建静态站点路由,可选统一登录守卫,适用于 portal.web、portal.auth.web 这类无动态菜单站点。
最小示例:
import { createStaticAppRouter } from '@litianxiang/portal-core'
const router = createStaticAppRouter({
staticRoutes,
getUserStore: () => useUserStore(),
requireAuth: true
})auth
createLogoutToAuth(options):生成统一退出函数,负责清理用户态并跳转到认证站。calcInactivityAction(lastActiveTime, now, config):根据空闲时长计算是否应刷新 token 或强制退出。
最小示例:
import { createLogoutToAuth, calcInactivityAction } from '@litianxiang/portal-core'
const logoutToAuth = createLogoutToAuth({
getUserStore: () => useUserStore(),
getLoadingStore: () => useLoadingStore()
})
const { shouldLogout, shouldRefresh } = calcInactivityAction(lastActiveTime, Date.now())
if (shouldLogout) logoutToAuth()
if (shouldRefresh) {
// 执行刷新 token
}http
createAuthHttpClient(options):创建带认证拦截器的 HTTP 客户端(Loading、token 注入、401 处理、刷新 token)。
最小示例:
import axios from 'axios'
import { createAuthHttpClient } from '@litianxiang/portal-core'
const { http } = createAuthHttpClient({
axios,
getUserStore: () => useUserStore(),
getLoadingStore: () => useLoadingStore()
})
await http.post('/api/demo', {})config
getDefaultAuthLoginUrl():基于当前域名生成默认统一登录地址。
最小示例:
import { getDefaultAuthLoginUrl } from '@litianxiang/portal-core'
const authLoginUrl = getDefaultAuthLoginUrl()permission
createPermissionHelper(options):创建权限判断工具(页面权限、按钮权限、任一权限命中)。
最小示例:
import { createPermissionHelper } from '@litianxiang/portal-core'
const permission = createPermissionHelper({ getUserStore: () => useUserStore() })
const canVisit = permission.hasPagePermission('/system/user')
const canCreate = permission.hasButtonPermission('/system/user:add')menu
createMenuHelper(options):创建菜单辅助工具,提供路径查找、标题、父链、面包屑能力。transformMenuTree(menuList, options):将后端菜单转换为标准CoreMenuItem结构。buildAllMenuTree(menuList):构建完整菜单树(含页面与按钮)。buildPageMenuList(menuList):从完整树裁剪 page/button 结构。findFirstPagePath(menuList):递归查找第一个可访问页面路径。
最小示例:
import {
createMenuHelper,
transformMenuTree,
buildAllMenuTree,
buildPageMenuList,
findFirstPagePath
} from '@litianxiang/portal-core'
const tree = transformMenuTree(rawMenu)
const allMenu = buildAllMenuTree(tree)
const pageMenu = buildPageMenuList(allMenu)
const firstPath = findFirstPagePath(allMenu)
const helper = createMenuHelper({ getMenuTree: () => allMenu })
const breadcrumb = helper.getBreadcrumb('/system/user')user
createBaseUserState():创建基础用户状态(不含菜单)。createBaseUserGetters():创建基础用户 getters(工号/姓名/token/最后活跃时间)。createBaseUserActions(options?):创建基础用户 actions(set/clear/syncFromAuthStore)。createMenuUserState():创建管理站点用户状态(含 all_menu/sidebar_menu/menuLoaded)。createMenuUserGetters():创建管理站点用户 getters(含菜单相关 getters)。createMenuUserActions(options):创建管理站点 actions(菜单加载、SSO 恢复、状态维护)。filterSidebarMenu(menus, options?):递归过滤不应在侧边栏展示的菜单类别。syncFromAuthStoreToStore(store, options?):从统一登录缓存恢复用户信息到 store。fetchUserMenuForStore(store, options):请求后端菜单并写入all_menu/sidebar_menu。
最小示例:
import {
createBaseUserState,
createBaseUserGetters,
createBaseUserActions,
createMenuUserState,
createMenuUserGetters,
createMenuUserActions,
filterSidebarMenu,
syncFromAuthStoreToStore,
fetchUserMenuForStore
} from '@litianxiang/portal-core'
// 轻量站点
const baseState = createBaseUserState()
const baseGetters = createBaseUserGetters()
const baseActions = createBaseUserActions()
// 管理站点
const menuState = createMenuUserState()
const menuGetters = createMenuUserGetters()
const menuActions = createMenuUserActions({ http, site: 'manage', transformMenuData, getALLMenu })
const restored = syncFromAuthStoreToStore(menuState)
const sidebar = filterSidebarMenu(menuState.all_menu)
await fetchUserMenuForStore(menuState, { http, site: 'manage', transformMenuData, getALLMenu })tab
addTab(tabs, tab):新增标签页(同路径不重复)。removeTab(tabs, path):按路径删除标签页。closeOthers(tabs, path, homePath?):关闭除当前与首页外的标签页。closeLeft(tabs, path, homePath?):关闭当前左侧标签页(首页保留)。closeRight(tabs, path, homePath?):关闭当前右侧标签页(首页保留)。closeAll(tabs):关闭全部可关闭标签页。createTabStoreOptions(homePath?):生成可直接用于 Pinia 的 Tab Store 配置。
最小示例:
import {
addTab,
removeTab,
closeOthers,
closeLeft,
closeRight,
closeAll,
createTabStoreOptions
} from '@litianxiang/portal-core'
let tabs = addTab([], { title: '首页', path: '/main/home', closable: false, keepAlive: true })
tabs = addTab(tabs, { title: '用户管理', path: '/system/user', closable: true, keepAlive: true })
tabs = closeOthers(tabs, '/system/user')
tabs = closeLeft(tabs, '/system/user')
tabs = closeRight(tabs, '/system/user')
tabs = removeTab(tabs, '/system/user')
tabs = closeAll(tabs)
const tabStoreOptions = createTabStoreOptions('/main/home')loading
createLoadingStoreOptions():生成可直接用于 Pinia 的 Loading Store 配置。
最小示例:
import { createLoadingStoreOptions } from '@litianxiang/portal-core'
const loadingStoreOptions = createLoadingStoreOptions()time
formatTime(date, format?):按指定格式输出时间字符串。humanizeTime(date):输出相对时间(如“3分钟前”)。timeDiff(start, end?):计算时间差(毫秒)。formatToMonthDay(date):格式化为MM-DD。createRange(start, end):生成有序时间区间(自动处理先后)。formatRange(start, end, format?):格式化时间区间字符串。getPresetRange(key, now?):获取常用预设区间(今日/近7天/本月等)。isInRange(value, range):判断时间是否落在区间内(含边界)。useTime():组合导出以上时间工具。
最小示例:
import {
formatTime,
humanizeTime,
timeDiff,
formatToMonthDay,
createRange,
formatRange,
getPresetRange,
isInRange,
useTime
} from '@litianxiang/portal-core'
const t1 = formatTime(new Date())
const t2 = humanizeTime(Date.now() - 60 * 1000)
const diff = timeDiff('2026-01-01')
const md = formatToMonthDay(new Date())
const range = createRange('2026-01-01', '2026-01-31')
const text = formatRange('2026-01-01', '2026-01-31')
const preset = getPresetRange('last7days')
const inRange = isInRange(new Date(), range)
const timeUtils = useTime()number
formatToYi(value, decimals?):按“亿元”单位格式化数值。formatToWanShou(value, decimals?):按“万手”单位格式化数值。formatWanYuanToYi(value, decimals?):将“万元”转换并格式化为“亿元”。
最小示例:
import { formatToYi, formatToWanShou, formatWanYuanToYi } from '@litianxiang/portal-core'
const a = formatToYi(123000000)
const b = formatToWanShou(560000)
const c = formatWanYuanToYi(50000)theme
initTheme():从本地缓存读取主题配置并初始化。applyTheme(config):将主题变量应用到根节点 CSS 变量。useThemeWatcher(config):监听主题变化并自动持久化。
最小示例:
import { initTheme, applyTheme, useThemeWatcher } from '@litianxiang/portal-core'
const theme = initTheme()
applyTheme(theme)
useThemeWatcher(theme)i18n
createPortalI18n(options):创建并返回统一配置的vue-i18n实例。
最小示例:
import { createPortalI18n } from '@litianxiang/portal-core'
const i18n = createPortalI18n({
messages: {
'zh-CN': { common: { ok: '确定' } },
'en-US': { common: { ok: 'OK' } }
}
})业务方约定
userStore(最小能力)
getUserAccessTokengetUserRefreshTokengetUserLastActiveTimesetUserAccessToken(token)setUserRefreshToken(token)setUserLastActiveTime(ts)syncFromAuthStore()clearUserInfo()
管理站点再补充:
isMenuLoadedresetMenuLoaded()fetchUserMenu()getUserALLMenu
路由结构约定
createAppRouter 会把动态菜单路由挂到 name: 'main' 的节点下,业务静态路由需提供该节点。
视图解析约定
菜单 path 会按以下优先级解析视图:
/src/views/main${path}.vue/src/views${path}.vue
常见问题
1) 刷新后跳登录
先检查 syncFromAuthStore() 是否正确从统一登录缓存恢复 token。
2) 401 循环跳转
检查是否重复创建多个 http 实例,或在 401 处理中重复手动跳转。
3) 菜单加载后白屏
检查菜单 path 与实际视图文件是否匹配(含 /main 约定)。
本地开发
pnpm install
pnpm run build
pnpm run pack:check发布流程
- 更新版本号(patch 递增)
- 执行
pnpm run pack:check - 发布:
npm publish --access public --registry=https://registry.npmjs.org/
