275 lines
8.8 KiB
TypeScript
275 lines
8.8 KiB
TypeScript
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,
|
||
}
|
||
})
|