feat: rbac接入
This commit is contained in:
+23
-9
@@ -5,7 +5,8 @@ import AdminLayout from '@/layouts/AdminLayout'
|
||||
import LoginPage from '@/pages/LoginPage'
|
||||
import ErrorBoundary from '@/components/ErrorBoundary'
|
||||
import { Suspense, useMemo, lazy, useEffect } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Loader2, Shield } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { SystemMenu } from '@/api/system'
|
||||
|
||||
const pages = import.meta.glob('./pages/**/*.tsx')
|
||||
@@ -25,14 +26,27 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore(s => s.isAuthenticated)
|
||||
if (isAuthenticated) return <Navigate to="/dashboard" replace />
|
||||
if (isAuthenticated) return <Navigate to="/" replace />
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function NoPermission() {
|
||||
const logout = useAuthStore(s => s.logout)
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center w-full">
|
||||
<Shield className="h-16 w-16 text-muted-foreground/30 mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2 text-foreground">访问受限</h2>
|
||||
<p className="text-muted-foreground mb-6 max-w-md">抱歉,您当前暂无任何系统权限,请联系管理员为您分配相关菜单与角色。</p>
|
||||
<Button onClick={logout} variant="default" className="w-32">退出登录</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
const menus = useAuthStore(s => s.menus)
|
||||
const isAuthenticated = useAuthStore(s => s.isAuthenticated)
|
||||
const refreshMenus = useAuthStore(s => s.refreshMenus)
|
||||
const hasFetchedMenus = useAuthStore(s => s.hasFetchedMenus)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && menus.length === 0) refreshMenus()
|
||||
@@ -59,17 +73,17 @@ function AppRoutes() {
|
||||
<Routes>
|
||||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||||
<Route path="/" element={<ProtectedRoute><AdminLayout /></ProtectedRoute>}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route index element={
|
||||
hasFetchedMenus ? (
|
||||
dynamicRoutes.length > 0 ? <Navigate to={dynamicRoutes[0].path} replace /> : <Navigate to="/403" replace />
|
||||
) : Loading
|
||||
} />
|
||||
{dynamicRoutes.map(({ path, Component }) => (
|
||||
<Route key={path} path={path.startsWith('/') ? path.substring(1) : path}
|
||||
element={<ErrorBoundary><Suspense fallback={Loading}><Component /></Suspense></ErrorBoundary>} />
|
||||
))}
|
||||
{!dynamicRoutes.some(r => r.path === '/dashboard') && dynamicComponentMap['/dashboard'] && (
|
||||
<Route path="dashboard" element={
|
||||
<ErrorBoundary><Suspense fallback={Loading}>
|
||||
{(() => { const D = dynamicComponentMap['/dashboard']; return <D /> })()}
|
||||
</Suspense></ErrorBoundary>
|
||||
} />
|
||||
{hasFetchedMenus && dynamicRoutes.length === 0 && (
|
||||
<Route path="*" element={<NoPermission />} />
|
||||
)}
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
||||
+6
-12
@@ -1,13 +1,13 @@
|
||||
import { get, post } from '@/lib/request'
|
||||
import { USE_MOCK, delay, mockResponse } from '@/mock'
|
||||
import { mockUsers } from '@/mock/system/users'
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
export interface SystemUser {
|
||||
id: string; account: string; name: string; nickName?: string; phone?: string
|
||||
avatar?: SystemOss; avatarId?: string; clientId?: string; tenantId?: string
|
||||
createdAt: string; updatedAt: string; roles?: SystemRole[]
|
||||
createdAt: string; updatedAt: string; roles?: string[] // Adjusted based on backend info
|
||||
menus?: any[]
|
||||
gender?: number
|
||||
}
|
||||
|
||||
export interface SystemRole {
|
||||
@@ -44,26 +44,20 @@ export interface OperationLog {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface CaptchaRes { captcha: string; captchaId: string }
|
||||
export interface CaptchaRes { captchaImg: string; captchaId: string }
|
||||
export interface LoginParams { account: string; password: string; captcha: string; captchaId: string }
|
||||
export interface LoginResponse { token: string; expiresAt: number; user: SystemUser }
|
||||
export interface LoginResponse { token: string; userInfo: SystemUser }
|
||||
|
||||
// ==================== Auth ====================
|
||||
|
||||
export async function getCaptcha() {
|
||||
if (USE_MOCK) { await delay(200); return mockResponse({ captcha: 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=', captchaId: 'mock-captcha-id' }) }
|
||||
return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha')
|
||||
}
|
||||
|
||||
export async function login(data: LoginParams) {
|
||||
if (USE_MOCK) { await delay(500); return mockResponse({ token: 'mock-token-xxx', expiresAt: Date.now() + 7200000, user: mockUsers[0] }, '登录成功') }
|
||||
return post<{ code: number; data: LoginResponse; msg: string }>('/auth/login', data)
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
if (USE_MOCK) { await delay(100); return mockResponse(null, '已登出') }
|
||||
return get<{ code: number; data: null; msg: string }>('/auth/logout')
|
||||
return get<{ code: number; data: null; msg: string }>('/auth/logout') // If backend doesn't have it, we just clear local token
|
||||
}
|
||||
|
||||
// ==================== 以下在各自文件中实现 ====================
|
||||
// 这里只导出类型,具体 CRUD 在 api/system/*.ts 子文件中
|
||||
|
||||
+31
-99
@@ -1,188 +1,120 @@
|
||||
import { get, post, type PageResult, type PageParams } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate, mockId, mockResponse } from '@/mock'
|
||||
import { mockUsers } from '@/mock/system/users'
|
||||
import { mockRoles } from '@/mock/system/roles'
|
||||
import { mockMenuTree } from '@/mock/system/menus'
|
||||
import { mockClients } from '@/mock/system/clients'
|
||||
import { mockFiles } from '@/mock/system/files'
|
||||
import { mockLogs } from '@/mock/system/logs'
|
||||
import type { SystemUser, SystemRole, SystemMenu, SystemClient, SystemOss, OperationLog } from './system'
|
||||
|
||||
// ==================== User ====================
|
||||
|
||||
export async function getUserList(data: PageParams & { account?: string; phone?: string }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let filtered = [...mockUsers]
|
||||
if (data.keyword) filtered = filtered.filter(u => u.name.includes(data.keyword!) || u.account.includes(data.keyword!))
|
||||
return mockResponse(paginate(filtered, data.current, data.pageSize))
|
||||
}
|
||||
return post<{ data: PageResult<SystemUser> }>('/user/getUserList', data)
|
||||
export async function getUserList(data: PageParams & { account?: string; phone?: string; name?: string }) {
|
||||
// map keyword to name or account if needed, or backend can handle it
|
||||
const reqData = { ...data, name: data.keyword, account: data.keyword }
|
||||
return post<{ data: PageResult<SystemUser> }>('/sys/user/list', reqData)
|
||||
}
|
||||
|
||||
export async function saveUser(data: Partial<SystemUser>) {
|
||||
if (USE_MOCK) { await delay(); mockUsers.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemUser); return mockResponse(null, '创建成功') }
|
||||
return post<{ msg: string }>('/user/save', data)
|
||||
return post<{ msg: string }>('/sys/user/create', data)
|
||||
}
|
||||
|
||||
export async function updateUser(data: Partial<SystemUser>) {
|
||||
if (USE_MOCK) { await delay(); const i = mockUsers.findIndex(u => u.id === data.id); if (i >= 0) Object.assign(mockUsers[i], data); return mockResponse(null, '更新成功') }
|
||||
return post<{ msg: string }>('/user/update', data)
|
||||
return post<{ msg: string }>('/sys/user/update', data)
|
||||
}
|
||||
|
||||
export async function deleteUser(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockUsers.findIndex(u => u.id === id); if (i >= 0) mockUsers.splice(i, 1) }); return mockResponse(null, '删除成功') }
|
||||
return post<{ msg: string }>('/user/delete', { ids })
|
||||
return post<{ msg: string }>('/sys/user/delete', { ids })
|
||||
}
|
||||
|
||||
export async function changePassword(data: { id: string; newPwd: string }) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '密码修改成功') }
|
||||
return post<{ msg: string }>('/user/changePassword', data)
|
||||
// admin reset password
|
||||
return post<{ msg: string }>('/sys/user/resetPassword', { id: data.id, password: data.newPwd })
|
||||
}
|
||||
|
||||
export async function grantRole(data: { userId: string; roleIds: string[] }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
const user = mockUsers.find(u => u.id === data.userId)
|
||||
if (user) user.roles = mockRoles.filter(r => data.roleIds.includes(r.id)).map(r => ({ ...r }))
|
||||
return mockResponse(null, '角色分配成功')
|
||||
}
|
||||
return post<{ msg: string }>('/user/grantRole', data)
|
||||
// backend UserUpdateReq has RoleIds
|
||||
return post<{ msg: string }>('/sys/user/update', { id: data.userId, roleIds: data.roleIds })
|
||||
}
|
||||
|
||||
// ==================== Role ====================
|
||||
|
||||
export async function getRoleList(data: PageParams & { name?: string }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let filtered = [...mockRoles]
|
||||
if (data.keyword) filtered = filtered.filter(r => r.name.includes(data.keyword!))
|
||||
return mockResponse(paginate(filtered, data.current, data.pageSize))
|
||||
}
|
||||
return post<{ data: PageResult<SystemRole> }>('/role/getRoleList', data)
|
||||
return post<{ data: PageResult<SystemRole> }>('/sys/role/list', { ...data, name: data.keyword })
|
||||
}
|
||||
|
||||
export async function getAllRoles() {
|
||||
if (USE_MOCK) { await delay(100); return mockResponse([...mockRoles]) }
|
||||
return post<{ data: SystemRole[] }>('/role/getAllRoles', {})
|
||||
return post<{ data: { list: SystemRole[] } }>('/sys/role/list', { current: 1, pageSize: 1000 })
|
||||
}
|
||||
|
||||
export async function saveRole(data: Partial<SystemRole>) {
|
||||
if (USE_MOCK) { await delay(); mockRoles.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemRole); return mockResponse(null, '创建成功') }
|
||||
return post<{ msg: string }>('/role/save', data)
|
||||
return post<{ msg: string }>('/sys/role/create', data)
|
||||
}
|
||||
|
||||
export async function updateRole(data: Partial<SystemRole>) {
|
||||
if (USE_MOCK) { await delay(); const i = mockRoles.findIndex(r => r.id === data.id); if (i >= 0) Object.assign(mockRoles[i], data); return mockResponse(null, '更新成功') }
|
||||
return post<{ msg: string }>('/role/update', data)
|
||||
return post<{ msg: string }>('/sys/role/update', data)
|
||||
}
|
||||
|
||||
export async function deleteRole(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockRoles.findIndex(r => r.id === id); if (i >= 0) mockRoles.splice(i, 1) }); return mockResponse(null, '删除成功') }
|
||||
return post<{ msg: string }>('/role/delete', { ids })
|
||||
return post<{ msg: string }>('/sys/role/delete', { ids })
|
||||
}
|
||||
|
||||
export async function grantMenu(data: { roleId: string; menuIds: string[] }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
const role = mockRoles.find(r => r.id === data.roleId)
|
||||
if (role) {
|
||||
const flatMenus = (items: SystemMenu[]): SystemMenu[] => items.flatMap(m => [m, ...(m.children ? flatMenus(m.children) : [])])
|
||||
role.menus = flatMenus(mockMenuTree).filter(m => data.menuIds.includes(m.id))
|
||||
}
|
||||
return mockResponse(null, '菜单授权成功')
|
||||
}
|
||||
return post<{ msg: string }>('/role/grantMenu', data)
|
||||
return post<{ msg: string }>('/sys/role/update', { id: data.roleId, menuIds: data.menuIds })
|
||||
}
|
||||
|
||||
// ==================== Menu ====================
|
||||
|
||||
export async function getAllMenuTree() {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) }
|
||||
return post<{ data: SystemMenu[] }>('/menu/getAllMenuTree', {})
|
||||
return get<{ data: { list: SystemMenu[] } }>('/sys/menu/list')
|
||||
}
|
||||
|
||||
export async function getUserMenuTree() {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) }
|
||||
return get<{ data: SystemMenu[] }>('/menu/getUserMenuTree')
|
||||
// Get current user info (including menus)
|
||||
return get<{ data: SystemUser }>('/auth/info')
|
||||
}
|
||||
|
||||
export async function saveMenu(data: Partial<SystemMenu>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') }
|
||||
return post<{ msg: string }>('/menu/save', data)
|
||||
return post<{ msg: string }>('/sys/menu/create', data)
|
||||
}
|
||||
|
||||
export async function updateMenu(data: Partial<SystemMenu>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') }
|
||||
return post<{ msg: string }>('/menu/update', data)
|
||||
return post<{ msg: string }>('/sys/menu/update', data)
|
||||
}
|
||||
|
||||
export async function deleteMenu(id: string) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') }
|
||||
return get<{ msg: string }>('/menu/delete', { id })
|
||||
return post<{ msg: string }>('/sys/menu/delete', { ids: [id] })
|
||||
}
|
||||
|
||||
// ==================== Client ====================
|
||||
|
||||
export async function getClientList(data: PageParams & { clientId?: string; name?: string }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let filtered = [...mockClients]
|
||||
if (data.keyword) filtered = filtered.filter(c => c.name.includes(data.keyword!) || c.clientId.includes(data.keyword!))
|
||||
return mockResponse(paginate(filtered, data.current, data.pageSize))
|
||||
}
|
||||
return post<{ data: PageResult<SystemClient> }>('/client/getClientList', data)
|
||||
return post<{ data: PageResult<SystemClient> }>('/sys/client/list', { ...data, name: data.keyword })
|
||||
}
|
||||
|
||||
export async function saveClient(data: Partial<SystemClient>) {
|
||||
if (USE_MOCK) { await delay(); mockClients.push({ ...data, id: mockId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as SystemClient); return mockResponse(null, '创建成功') }
|
||||
return post<{ msg: string }>('/client/save', data)
|
||||
return post<{ msg: string }>('/sys/client/create', data)
|
||||
}
|
||||
|
||||
export async function updateClient(data: Partial<SystemClient>) {
|
||||
if (USE_MOCK) { await delay(); const i = mockClients.findIndex(c => c.id === data.id); if (i >= 0) Object.assign(mockClients[i], data); return mockResponse(null, '更新成功') }
|
||||
return post<{ msg: string }>('/client/update', data)
|
||||
return post<{ msg: string }>('/sys/client/update', data)
|
||||
}
|
||||
|
||||
export async function deleteClient(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockClients.findIndex(c => c.id === id); if (i >= 0) mockClients.splice(i, 1) }); return mockResponse(null, '删除成功') }
|
||||
return post<{ msg: string }>('/client/delete', { ids })
|
||||
return post<{ msg: string }>('/sys/client/delete', { ids })
|
||||
}
|
||||
|
||||
// ==================== File ====================
|
||||
|
||||
export async function getFileList(data: PageParams & { name?: string }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let filtered = [...mockFiles]
|
||||
if (data.keyword) filtered = filtered.filter(f => f.name.includes(data.keyword!))
|
||||
return mockResponse(paginate(filtered, data.current, data.pageSize))
|
||||
}
|
||||
return post<{ data: PageResult<SystemOss> }>('/oss/getFileList', data)
|
||||
return post<{ data: PageResult<SystemOss> }>('/file/oss/getFileList', data)
|
||||
}
|
||||
|
||||
export async function uploadFile(_file: File) {
|
||||
if (USE_MOCK) { await delay(800); const f: SystemOss = { id: mockId(), name: _file.name, key: `uploads/${_file.name}`, url: `https://picsum.photos/seed/${Date.now()}/400/300`, suffix: _file.name.split('.').pop() || '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; mockFiles.unshift(f); return mockResponse({ file: f }, '上传成功') }
|
||||
const formData = new FormData(); formData.append('file', _file)
|
||||
return post<{ data: { file: SystemOss }; msg: string }>('/oss/upload', formData)
|
||||
return post<{ data: { file: SystemOss }; msg: string }>('/file/oss/upload', formData)
|
||||
}
|
||||
|
||||
export async function deleteFile(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); ids.forEach(id => { const i = mockFiles.findIndex(f => f.id === id); if (i >= 0) mockFiles.splice(i, 1) }); return mockResponse(null, '删除成功') }
|
||||
return post<{ msg: string }>('/oss/delete', { ids })
|
||||
return post<{ msg: string }>('/file/oss/delete', { ids })
|
||||
}
|
||||
|
||||
// ==================== Operation Log ====================
|
||||
|
||||
export async function getOperationLogList(data: PageParams & { clientId?: string; method?: string; statusCode?: number }) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let filtered = [...mockLogs]
|
||||
if (data.clientId) filtered = filtered.filter(l => l.clientId === data.clientId)
|
||||
if (data.method) filtered = filtered.filter(l => l.method === data.method)
|
||||
if (data.statusCode !== undefined) filtered = filtered.filter(l => l.statusCode === data.statusCode)
|
||||
if (data.keyword) filtered = filtered.filter(l => l.path.includes(data.keyword!) || l.title.includes(data.keyword!) || l.operatorName.includes(data.keyword!))
|
||||
return mockResponse(paginate(filtered, data.current, data.pageSize))
|
||||
}
|
||||
return post<{ data: PageResult<OperationLog> }>('/log/getOperationLogList', data)
|
||||
return post<{ data: PageResult<OperationLog> }>('/sys/log/list', data)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ function convertMenuToNavItem(menu: SystemMenu): NavItem {
|
||||
href: menu.path || menu.code || `/${menu.name.toLowerCase()}`,
|
||||
icon: getIcon(menu.icon),
|
||||
permission: menu.permission,
|
||||
children: menu.children?.map(convertMenuToNavItem),
|
||||
children: menu.children?.filter(c => c.category !== 2).length ? menu.children.filter(c => c.category !== 2).map(convertMenuToNavItem) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +191,8 @@ export default function AdminLayout() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const navItems = useMemo(() => {
|
||||
if (menus?.length) return menus.map(convertMenuToNavItem)
|
||||
return [{ title: '仪表盘', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> }]
|
||||
if (menus?.length) return menus.filter(m => m.category !== 2).map(convertMenuToNavItem)
|
||||
return []
|
||||
}, [menus])
|
||||
|
||||
const handleLogout = async () => { await logout(); navigate('/login') }
|
||||
|
||||
@@ -12,6 +12,7 @@ const request = axios.create({
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
config.headers['X-Client-Id'] = 'sundynix-admin'
|
||||
const isWhitelisted = AUTH_WHITELIST.some(path => config.url?.startsWith(path))
|
||||
if (!isWhitelisted) {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function LoginPage() {
|
||||
const res = await getCaptcha()
|
||||
const d = (res as any).data
|
||||
setCaptchaId(d.captchaId)
|
||||
setCaptchaImg(d.captcha)
|
||||
setCaptchaImg(d.captchaImg)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function LoginPage() {
|
||||
try {
|
||||
const res = await apiLogin({ account, password, captcha, captchaId })
|
||||
const d = (res as any).data
|
||||
loginStore(d.user, d.token)
|
||||
loginStore(d.userInfo, d.token)
|
||||
navigate('/dashboard', { replace: true })
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '登录失败')
|
||||
@@ -171,8 +171,6 @@ export default function LoginPage() {
|
||||
: '登 录'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-xs text-slate-400 mt-6">Mock 模式 · 任意账号密码均可登录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-5
@@ -12,6 +12,7 @@ interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
menus: SystemMenu[]
|
||||
permissions: string[]
|
||||
hasFetchedMenus: boolean
|
||||
login: (user: SystemUser, token: string) => void
|
||||
logout: () => Promise<void>
|
||||
refreshMenus: () => Promise<void>
|
||||
@@ -47,27 +48,31 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
isAuthenticated: initial.isAuthenticated,
|
||||
menus: [],
|
||||
permissions: [],
|
||||
hasFetchedMenus: false,
|
||||
|
||||
login: (user, token) => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user))
|
||||
set({ user, token, isAuthenticated: true, menus: [], permissions: [] })
|
||||
set({ user, token, isAuthenticated: true, menus: [], permissions: [], hasFetchedMenus: false })
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try { await apiLogout() } catch { /* ignore */ }
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
set({ user: null, token: null, isAuthenticated: false, menus: [], permissions: [] })
|
||||
set({ user: null, token: null, isAuthenticated: false, menus: [], permissions: [], hasFetchedMenus: false })
|
||||
},
|
||||
|
||||
refreshMenus: async () => {
|
||||
if (!get().isAuthenticated) return
|
||||
try {
|
||||
const res = await getUserMenuTree()
|
||||
const menus = res.data || []
|
||||
set({ menus, permissions: extractPermissions(menus) })
|
||||
} catch (e) { console.error('获取菜单失败:', e) }
|
||||
const menus = (res.data as any).menus || []
|
||||
set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true })
|
||||
} catch (e) {
|
||||
console.error('获取菜单失败:', e)
|
||||
set({ hasFetchedMenus: true })
|
||||
}
|
||||
},
|
||||
|
||||
hasPermission: (permission) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8888',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user