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(null) const roles = ref([]) const menus = ref([]) const token = ref('') const loading = ref(false) // CheckAdmin 结果(后端按当前角色权限码判定管理员,不依赖 GetELTUserInfo 的 IsAdmin 字段) const adminCheck = ref(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(() => { 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 { loading.value = true try { await apiLogin(params) await fetchUserInfo() await fetchMenus() await fetchRoles() await fetchAdminCheck() } finally { loading.value = false } } /** 注销 */ async function logout(): Promise { try { await apiLogout() } finally { reset() } } /** 获取当前用户信息 */ async function fetchUserInfo(): Promise { const info = await getCurrentUser() userInfo.value = info saveToCache() return info } /** 获取管理员状态(CheckAdmin,按当前角色权限码判定,失败不阻塞主流程) */ async function fetchAdminCheck(): Promise { try { adminCheck.value = await get('/API/FormAPI/CheckAdmin') } catch { // 失败保持上一次结果(或 null),不阻塞登录/恢复流程 } finally { // 无论成功失败都标记加载完成,页面据此区分"加载中"与"确实无权限" adminChecked.value = true } } /** * 静默恢复会话(路由守卫用):成功则填充用户信息,失败不弹全局提示 * 优先级:内存态 > localStorage 缓存 > 后端请求 * - 内存已有 userInfo:直接返回(同会话内已恢复过,零请求) * - 缓存命中:从 localStorage 恢复(刷新后首次,零请求) * - 都没有:请求后端 GetELTUserInfo + MenuList(首次登录或缓存被清) * 会话过期兜底:缓存命中后,首个业务请求 401 会触发 handleLoginExpired 清缓存并跳登录 */ async function restoreSession(): Promise { // 内存已有:直接返回 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 { const list = await getUserRoles() roles.value = list saveToCache() return list } /** 切换当前角色(身份切换) */ async function switchRole(role: RoleItem): Promise { 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 { 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, } })