import { computed, ref } from 'vue' import { defineStore } from 'pinia' import { changePassword as changePasswordRequest, getCurrentAuthorization, getCurrentUser, login as loginRequest, logout as logoutRequest, } from '@/api/auth' import type { AuthenticatedUser, LoginRequest, LoginResponse, CurrentAuthorization, PermissionResourceNode, PasswordChangeRequest, } from '@/types/auth' import { clearAuthStorage, readStoredAuthSession, saveAuthSession } from '@/utils/auth-storage' import { useAppStore } from '@/stores/app' export const useLoginStore = defineStore('login', () => { const storedSession = readStoredAuthSession() const token = ref(storedSession?.token ?? '') const expiresAt = ref(storedSession?.expiresAt ?? '') const user = ref(storedSession?.user ?? null) const authorization = ref(null) const currentUserReady = ref(false) let currentUserRequest: Promise | null = null let authorizationSyncedAt = 0 const isLoggedIn = computed(() => Boolean(token.value)) // Data scope belongs to roles now; it no longer determines whether a user can write. const isReadOnly = computed(() => false) const userName = computed(() => user.value?.displayName || user.value?.username || '医护用户') const department = computed(() => user.value?.departmentName || '医签通用户') function hasPermission(required: string | string[]) { const requiredPermissions = Array.isArray(required) ? required : [required] if (!requiredPermissions.length) { return true } const permissions = authorization.value?.permissions ?? user.value?.permissions ?? [] return requiredPermissions.every( (permission) => permissions.includes(permission) || permissions.includes('*'), ) } /** * 已下线菜单:后端菜单资源里仍然下发、但前端已经不再提供页面的菜单。 * * 「系统管理 → 报表分析」(`/report/index`)已整体下线,后端菜单资源暂时无法同步下架, * 所以在前端统一屏蔽:侧边栏不展示,路由守卫也会把它当成无权限菜单重定向走, * 避免出现点进去只有「前端暂未接入组件」占位页的死链。 * 后端下架该菜单资源后,这个集合可以删掉。 */ const RETIRED_MENU_PATHS = new Set(['/report/index']) function isRetiredMenu(menu: PermissionResourceNode) { return RETIRED_MENU_PATHS.has(menu.routePath?.replace(/\/$/, '') ?? '') } const accessibleMenus = computed(() => (authorization.value?.navigation ?? []) .filter((directory) => directory.resourceType === 'DIR' && directory.status === 'ENABLED') .flatMap((directory) => directory.children ?? []) .filter((menu) => !isRetiredMenu(menu)), ) /** 侧边栏用的导航树:结构与后端一致,只剔除已下线菜单(空目录由使用方过滤) */ const visibleNavigation = computed(() => (authorization.value?.navigation ?? []) .filter((directory) => directory.resourceType === 'DIR' && directory.status === 'ENABLED') .map((directory) => ({ ...directory, children: (directory.children ?? []).filter((menu) => !isRetiredMenu(menu)), })), ) function menuForPath(path: string): PermissionResourceNode | null { const normalizedPath = path.replace(/\/$/, '') || '/' return ( accessibleMenus.value.find((menu) => menu.routePath?.replace(/\/$/, '') === normalizedPath) ?? null ) } const firstAuthorizedPath = computed( () => accessibleMenus.value.find( (menu) => menu.visible && menu.status === 'ENABLED' && menu.code && hasPermission(menu.code), )?.routePath ?? null, ) function hasWritePermission(required: string | string[]) { return !isReadOnly.value && hasPermission(required) } function setSession(session: LoginResponse) { saveAuthSession(session) token.value = session.token expiresAt.value = session.expiresAt user.value = session.user authorization.value = null authorizationSyncedAt = 0 currentUserReady.value = false } function setCurrentUser(nextUser: AuthenticatedUser) { user.value = nextUser if (token.value) { saveAuthSession({ token: token.value, expiresAt: expiresAt.value, user: nextUser, }) } } function clearSession() { clearAuthStorage() token.value = '' expiresAt.value = '' user.value = null authorization.value = null authorizationSyncedAt = 0 currentUserReady.value = false currentUserRequest = null // 会话结束后清掉跨账号的内存态(当前是院区列表与选中项), // 否则 SPA 内退出登录后换账号登录,新账号会看到上一个账号的院区。 useAppStore().reset() } async function login(payload: LoginRequest) { const session = await loginRequest(payload) setSession(session) return session } async function refreshCurrentUser() { const [currentUser, currentAuthorization] = await Promise.all([ getCurrentUser(), getCurrentAuthorization(), ]) const effectiveUser = { ...currentUser, permissions: currentAuthorization.permissions } authorization.value = currentAuthorization authorizationSyncedAt = Date.now() setCurrentUser(effectiveUser) return effectiveUser } async function refreshAuthorization() { const currentAuthorization = await getCurrentAuthorization() authorization.value = currentAuthorization authorizationSyncedAt = Date.now() if (user.value) { setCurrentUser({ ...user.value, permissions: currentAuthorization.permissions }) } return currentAuthorization } async function ensureCurrentUser() { if (!token.value) { return null } if ( currentUserReady.value && user.value && authorization.value && Date.now() - authorizationSyncedAt < 10_000 ) { return user.value } if (!currentUserRequest) { currentUserRequest = refreshCurrentUser() .then((currentUser) => { currentUserReady.value = true return currentUser }) .catch((error: unknown) => { currentUserReady.value = false throw error }) .finally(() => { currentUserRequest = null }) } return currentUserRequest } async function logout() { try { await logoutRequest() } finally { clearSession() } } async function changePassword(payload: PasswordChangeRequest) { await changePasswordRequest(payload) // 服务端会递增 authEpoch;当前令牌随即失效,必须使用新密码重新登录。 clearSession() } return { token, expiresAt, user, authorization, accessibleMenus, visibleNavigation, isLoggedIn, isReadOnly, userName, department, login, refreshCurrentUser, ensureCurrentUser, refreshAuthorization, menuForPath, firstAuthorizedPath, hasPermission, hasWritePermission, changePassword, logout, clearSession, } })