Files
GERP/src/stores/user.ts
T

275 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserInfo, MenuItem } from '@/types/api'
import { getCurrentUser, getUserRoles, switchRole as apiSwitchRole, login as apiLogin, logout as apiLogout, type LoginParams, type RoleItem } from '@/api/auth'
import { getMenuList } from '@/api/menu'
import { get } from '@/api/request'
/** CheckAdmin 接口返回的管理员状态(对齐后端 /API/FormAPI/CheckAdmin) */
export interface AdminCheckResult {
is_admin?: boolean
is_superuser?: boolean
is_super_admin?: boolean
is_tenant_admin?: boolean
current_role_name?: string
current_role_uuid?: string
role_scope?: {
Type?: string
TypeLabel?: string
IsAdmin?: boolean
IsDeptManager?: boolean
IsWorkgroupLead?: boolean
IsMember?: boolean
} | null
}
/** 用户状态管理:当前登录用户、角色、菜单、权限 */
export const useUserStore = defineStore('user', () => {
// === 状态 ===
const userInfo = ref<UserInfo | null>(null)
const roles = ref<RoleItem[]>([])
const menus = ref<MenuItem[]>([])
const token = ref<string>('')
const loading = ref(false)
// CheckAdmin 结果(后端按当前角色权限码判定管理员,不依赖 GetELTUserInfo 的 IsAdmin 字段)
const adminCheck = ref<AdminCheckResult | null>(null)
// CheckAdmin 是否已加载完成(成功或失败都置 true,供页面区分"加载中"与"确实无权限")
const adminChecked = ref(false)
// === localStorage 持久化缓存 ===
// 目的:刷新页面后从缓存恢复 userInfo/menus,避免重复请求 GetELTUserInfo/MenuList
// 复用 gerp-user key(reset/handleLoginExpired 已删除此 key,形成写入→清除闭环)
const CACHE_KEY = 'gerp-user'
/** 从 localStorage 恢复缓存到内存(刷新后内存丢失时调用) */
function loadFromCache(): void {
try {
const raw = localStorage.getItem(CACHE_KEY)
if (!raw) return
const cached = JSON.parse(raw) as { userInfo?: UserInfo | null; menus?: MenuItem[] | null; roles?: RoleItem[] | null }
if (cached.userInfo) userInfo.value = cached.userInfo
if (cached.menus) menus.value = cached.menus
if (cached.roles?.length) roles.value = cached.roles
} catch {
// 缓存损坏:清除避免反复解析失败
localStorage.removeItem(CACHE_KEY)
}
}
/** 将内存态写入 localStorage(fetchUserInfo/fetchMenus 成功后调用) */
function saveToCache(): void {
try {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ userInfo: userInfo.value, menus: menus.value, roles: roles.value }),
)
} catch {
// 写入失败(隐私模式/配额满):忽略,不影响功能
}
}
// store 初始化时立即从缓存恢复:刷新后 isLoggedIn 立即为 true,
// router.beforeEach 不再触发 restoreSession,实现刷新零请求
loadFromCache()
// === 计算属性 ===
const isLoggedIn = computed(() => !!userInfo.value)
// 管理员判定优先用 CheckAdmin(后端按当前角色 admin:super/admin:tenant 权限码判定);
// 未加载 CheckAdmin 时回退 userInfo.IsAdmin(旧缓存数据)
const isAdmin = computed(() => adminCheck.value?.is_admin === true || userInfo.value?.IsAdmin === true)
const currentRole = computed(() => userInfo.value?.CurrentRole || '')
const avatar = computed(() => userInfo.value?.Avatar || '')
const displayName = computed(() => userInfo.value?.DisplayName || userInfo.value?.UserName || '未登录')
/** 扁平化后的菜单(用于面包屑、菜单搜索) */
const flatMenus = computed<MenuItem[]>(() => {
const result: MenuItem[] = []
const walk = (items: MenuItem[]) => {
for (const item of items) {
result.push(item)
if (item.Children?.length) walk(item.Children)
}
}
walk(menus.value)
return result
})
/** 目标路径是否在用户可见菜单树中(含子菜单;供路由守卫校验 URL 直达私有页) */
function hasMenuPath(path: string): boolean {
if (!path) return false
const normalized = path.split('?')[0].replace(/\/+$/, '')
const walk = (items: MenuItem[]): boolean => {
for (const item of items) {
if (item.Path && item.Path === normalized) return true
if (item.Children?.length && walk(item.Children)) return true
}
return false
}
return walk(menus.value)
}
// === 动作 ===
/** 登录 */
async function login(params: LoginParams): Promise<void> {
loading.value = true
try {
await apiLogin(params)
await fetchUserInfo()
await fetchMenus()
await fetchRoles()
await fetchAdminCheck()
} finally {
loading.value = false
}
}
/** 注销 */
async function logout(): Promise<void> {
try {
await apiLogout()
} finally {
reset()
}
}
/** 获取当前用户信息 */
async function fetchUserInfo(): Promise<UserInfo> {
const info = await getCurrentUser()
userInfo.value = info
saveToCache()
return info
}
/** 获取管理员状态(CheckAdmin,按当前角色权限码判定,失败不阻塞主流程) */
async function fetchAdminCheck(): Promise<void> {
try {
adminCheck.value = await get<AdminCheckResult>('/API/FormAPI/CheckAdmin')
} catch {
// 失败保持上一次结果(或 null),不阻塞登录/恢复流程
} finally {
// 无论成功失败都标记加载完成,页面据此区分"加载中"与"确实无权限"
adminChecked.value = true
}
}
/**
* 静默恢复会话(路由守卫用):成功则填充用户信息,失败不弹全局提示
* 优先级:内存态 > localStorage 缓存 > 后端请求
* - 内存已有 userInfo:直接返回(同会话内已恢复过,零请求)
* - 缓存命中:从 localStorage 恢复(刷新后首次,零请求)
* - 都没有:请求后端 GetELTUserInfo + MenuList(首次登录或缓存被清)
* 会话过期兜底:缓存命中后,首个业务请求 401 会触发 handleLoginExpired 清缓存并跳登录
*/
async function restoreSession(): Promise<boolean> {
// 内存已有:直接返回
if (userInfo.value) return true
// 尝试从缓存恢复
loadFromCache()
if (userInfo.value) {
// 缓存恢复后异步刷新管理员状态(保证刷新后 isAdmin 正确)
void fetchAdminCheck()
return true
}
// 缓存也没有:请求后端
try {
const info = await getCurrentUser(true)
userInfo.value = info
await fetchMenus()
saveToCache()
await fetchAdminCheck()
return true
} catch {
return false
}
}
/** 获取角色列表 */
async function fetchRoles(): Promise<RoleItem[]> {
const list = await getUserRoles()
roles.value = list
saveToCache()
return list
}
/** 切换当前角色(身份切换) */
async function switchRole(role: RoleItem): Promise<void> {
await apiSwitchRole(role)
// 刷新用户信息和菜单(不同角色可能有不同菜单权限)
await fetchUserInfo()
await fetchMenus()
// 管理员状态按新角色重新判定
await fetchAdminCheck()
}
/** 将后端 Page(如 pages/workorder/admin.html)规范化为 Vue 路由(/workorder/admin) */
function normalizeMenuPath(page: string | undefined): string {
if (!page) return ''
let p = page.trim()
if (!p) return ''
if (/^https?:\/\//i.test(p)) return p
if (p.startsWith('#')) p = p.slice(1)
p = p.replace(/^\/+/, '')
if (p.startsWith('pages/')) p = p.slice(6)
p = p.replace(/\.html?$/i, '')
return p ? '/' + p : ''
}
/** 递归规范化菜单树:Page → Path */
function normalizeMenuTree(items: MenuItem[]): MenuItem[] {
return items.map((item) => ({
...item,
Path: normalizeMenuPath(item.Page),
Children: item.Children?.length ? normalizeMenuTree(item.Children) : [],
}))
}
/** 获取菜单 */
async function fetchMenus(): Promise<MenuItem[]> {
const list = await getMenuList()
const normalized = normalizeMenuTree(list)
menus.value = normalized
saveToCache()
return normalized
}
/** 重置状态(登出或登录失效时调用) */
function reset(): void {
userInfo.value = null
roles.value = []
menus.value = []
token.value = ''
adminCheck.value = null
adminChecked.value = false
localStorage.removeItem(CACHE_KEY)
}
return {
// 状态
userInfo,
roles,
menus,
token,
loading,
adminCheck,
adminChecked,
// 计算属性
isLoggedIn,
isAdmin,
currentRole,
avatar,
displayName,
flatMenus,
hasMenuPath,
// 动作
login,
logout,
fetchUserInfo,
fetchAdminCheck,
fetchRoles,
fetchMenus,
switchRole,
restoreSession,
reset,
}
})