build: 调整 JaCoCo 覆盖率检查配置 #7
@@ -1,13 +1,12 @@
|
|||||||
import { ConfigProvider } from 'antd'
|
import { ConfigProvider } from 'antd'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
|
import { RouterProvider } from 'react-router'
|
||||||
|
import { router } from '@/router'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<div style={{ padding: 24 }}>
|
<RouterProvider router={router} />
|
||||||
<h1>Novalon Manage System</h1>
|
|
||||||
<p>React 19 迁移进行中...</p>
|
|
||||||
</div>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Outlet } from 'react-router'
|
||||||
|
|
||||||
|
export default function DefaultLayout() {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<h1>DefaultLayout (TODO)</h1>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Result, Button } from 'antd'
|
||||||
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
|
export default function Forbidden() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="403"
|
||||||
|
title="403"
|
||||||
|
subTitle="抱歉,您没有权限访问此页面。"
|
||||||
|
extra={<Button type="primary" onClick={() => navigate('/dashboard')}>返回首页</Button>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function ExceptionLog() {
|
||||||
|
return <div>ExceptionLog Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function LoginLog() {
|
||||||
|
return <div>LoginLog Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function OperationLog() {
|
||||||
|
return <div>OperationLog Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function ConfigManagement() {
|
||||||
|
return <div>ConfigManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function DictManagement() {
|
||||||
|
return <div>DictManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Dashboard() {
|
||||||
|
return <div>Dashboard Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function FileManagement() {
|
||||||
|
return <div>FileManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Login() {
|
||||||
|
return <div>Login Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function NoticeManagement() {
|
||||||
|
return <div>NoticeManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function MenuManagement() {
|
||||||
|
return <div>MenuManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function RoleManagement() {
|
||||||
|
return <div>RoleManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function UserManagement() {
|
||||||
|
return <div>UserManagement Page (TODO)</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { redirect } from 'react-router'
|
||||||
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
|
import { usePermissionStore } from '@/stores/usePermissionStore'
|
||||||
|
|
||||||
|
export async function authLoader() {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return redirect('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const authState = useAuthStore.getState()
|
||||||
|
|
||||||
|
if (!authState.initialized) {
|
||||||
|
authState.initFromStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authState.isAuthenticated) {
|
||||||
|
return redirect('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const permState = usePermissionStore.getState()
|
||||||
|
|
||||||
|
if (!permState.loaded) {
|
||||||
|
const restored = permState.initFromStorage()
|
||||||
|
if (!restored) {
|
||||||
|
try {
|
||||||
|
await permState.fetchUserMenus()
|
||||||
|
} catch {
|
||||||
|
authState.logout()
|
||||||
|
return redirect('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { createBrowserRouter, Navigate } from 'react-router'
|
||||||
|
import { authLoader } from './guards'
|
||||||
|
import {
|
||||||
|
DefaultLayout,
|
||||||
|
Login,
|
||||||
|
Dashboard,
|
||||||
|
UserManagement,
|
||||||
|
RoleManagement,
|
||||||
|
MenuManagement,
|
||||||
|
ConfigManagement,
|
||||||
|
DictManagement,
|
||||||
|
FileManagement,
|
||||||
|
NoticeManagement,
|
||||||
|
LoginLog,
|
||||||
|
OperationLog,
|
||||||
|
ExceptionLog,
|
||||||
|
Forbidden,
|
||||||
|
} from './routes'
|
||||||
|
|
||||||
|
export const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/403',
|
||||||
|
element: <Forbidden />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <DefaultLayout />,
|
||||||
|
loader: authLoader,
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||||
|
{ path: 'dashboard', element: <Dashboard /> },
|
||||||
|
{ path: 'users', element: <UserManagement /> },
|
||||||
|
{ path: 'roles', element: <RoleManagement /> },
|
||||||
|
{ path: 'menus', element: <MenuManagement /> },
|
||||||
|
{ path: 'sys/config', element: <ConfigManagement /> },
|
||||||
|
{ path: 'dict', element: <DictManagement /> },
|
||||||
|
{ path: 'files', element: <FileManagement /> },
|
||||||
|
{ path: 'notice', element: <NoticeManagement /> },
|
||||||
|
{ path: 'loginlog', element: <LoginLog /> },
|
||||||
|
{ path: 'oplog', element: <OperationLog /> },
|
||||||
|
{ path: 'exceptionlog', element: <ExceptionLog /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { lazy } from 'react'
|
||||||
|
|
||||||
|
const Login = lazy(() => import('@/pages/login'))
|
||||||
|
const Dashboard = lazy(() => import('@/pages/dashboard'))
|
||||||
|
const UserManagement = lazy(() => import('@/pages/system/user'))
|
||||||
|
const RoleManagement = lazy(() => import('@/pages/system/role'))
|
||||||
|
const MenuManagement = lazy(() => import('@/pages/system/menu'))
|
||||||
|
const ConfigManagement = lazy(() => import('@/pages/config/config'))
|
||||||
|
const DictManagement = lazy(() => import('@/pages/config/dict'))
|
||||||
|
const FileManagement = lazy(() => import('@/pages/file'))
|
||||||
|
const NoticeManagement = lazy(() => import('@/pages/notify'))
|
||||||
|
const LoginLog = lazy(() => import('@/pages/audit/login-log'))
|
||||||
|
const OperationLog = lazy(() => import('@/pages/audit/operation-log'))
|
||||||
|
const ExceptionLog = lazy(() => import('@/pages/audit/exception-log'))
|
||||||
|
const Forbidden = lazy(() => import('@/pages/403'))
|
||||||
|
const DefaultLayout = lazy(() => import('@/layouts/DefaultLayout'))
|
||||||
|
|
||||||
|
export { authLoader } from './guards'
|
||||||
|
export { DefaultLayout, Login, Dashboard, UserManagement, RoleManagement, MenuManagement, ConfigManagement, DictManagement, FileManagement, NoticeManagement, LoginLog, OperationLog, ExceptionLog, Forbidden }
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
collapsed: boolean
|
||||||
|
toggleCollapsed: () => void
|
||||||
|
setCollapsed: (val: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = create<AppState>((set) => ({
|
||||||
|
collapsed: false,
|
||||||
|
toggleCollapsed: () => set((state) => ({ collapsed: !state.collapsed })),
|
||||||
|
setCollapsed: (val: boolean) => set({ collapsed: val }),
|
||||||
|
}))
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { jwtDecode } from 'jwt-decode'
|
||||||
|
import { authApi } from '@/api/auth.api'
|
||||||
|
import type { JwtPayload } from '@/types/user'
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
token: string | null
|
||||||
|
userId: number | null
|
||||||
|
username: string | null
|
||||||
|
nickname: string | null
|
||||||
|
roles: string[]
|
||||||
|
permissions: string[]
|
||||||
|
isAuthenticated: boolean
|
||||||
|
initialized: boolean
|
||||||
|
|
||||||
|
login: (username: string, password: string) => Promise<void>
|
||||||
|
logout: () => void
|
||||||
|
initFromStorage: () => void
|
||||||
|
setInitialized: (val: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||||
|
token: null,
|
||||||
|
userId: null,
|
||||||
|
username: null,
|
||||||
|
nickname: null,
|
||||||
|
roles: [],
|
||||||
|
permissions: [],
|
||||||
|
isAuthenticated: false,
|
||||||
|
initialized: false,
|
||||||
|
|
||||||
|
login: async (username: string, password: string) => {
|
||||||
|
const res = await authApi.login({ username, password })
|
||||||
|
const data = res as any
|
||||||
|
const token = data.token
|
||||||
|
const decoded = jwtDecode<JwtPayload>(token)
|
||||||
|
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
localStorage.setItem('userId', String(decoded.sub))
|
||||||
|
localStorage.setItem('username', decoded.username)
|
||||||
|
localStorage.setItem('roles', JSON.stringify(decoded.roles))
|
||||||
|
|
||||||
|
set({
|
||||||
|
token,
|
||||||
|
userId: Number(decoded.sub),
|
||||||
|
username: decoded.username,
|
||||||
|
roles: decoded.roles || [],
|
||||||
|
isAuthenticated: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { usePermissionStore } = await import('./usePermissionStore')
|
||||||
|
await usePermissionStore.getState().fetchUserMenus()
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userId')
|
||||||
|
localStorage.removeItem('username')
|
||||||
|
localStorage.removeItem('roles')
|
||||||
|
localStorage.removeItem('permission')
|
||||||
|
|
||||||
|
set({
|
||||||
|
token: null,
|
||||||
|
userId: null,
|
||||||
|
username: null,
|
||||||
|
nickname: null,
|
||||||
|
roles: [],
|
||||||
|
permissions: [],
|
||||||
|
isAuthenticated: false,
|
||||||
|
initialized: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
import('./usePermissionStore').then(({ usePermissionStore }) => {
|
||||||
|
usePermissionStore.getState().clearPermissionData()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
initFromStorage: () => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwtDecode<JwtPayload>(token)
|
||||||
|
if (decoded.exp * 1000 < Date.now()) {
|
||||||
|
get().logout()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
token,
|
||||||
|
userId: Number(decoded.sub),
|
||||||
|
username: decoded.username,
|
||||||
|
roles: decoded.roles || [],
|
||||||
|
isAuthenticated: true,
|
||||||
|
initialized: true,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
get().logout()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setInitialized: (val: boolean) => set({ initialized: val }),
|
||||||
|
}))
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { menuApi } from '@/api/menu'
|
||||||
|
import type { MenuItem } from '@/api/menu'
|
||||||
|
|
||||||
|
interface PermissionState {
|
||||||
|
roles: string[]
|
||||||
|
permissions: string[]
|
||||||
|
menus: MenuItem[]
|
||||||
|
loaded: boolean
|
||||||
|
|
||||||
|
fetchUserMenus: () => Promise<void>
|
||||||
|
hasPermission: (permission: string) => boolean
|
||||||
|
hasRole: (role: string) => boolean
|
||||||
|
clearPermissionData: () => void
|
||||||
|
initFromStorage: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePermissionStore = create<PermissionState>((set, get) => ({
|
||||||
|
roles: [],
|
||||||
|
permissions: [],
|
||||||
|
menus: [],
|
||||||
|
loaded: false,
|
||||||
|
|
||||||
|
fetchUserMenus: async () => {
|
||||||
|
try {
|
||||||
|
const res = await menuApi.getTree()
|
||||||
|
const menus = res as unknown as MenuItem[]
|
||||||
|
const permissions = extractPermissions(menus)
|
||||||
|
|
||||||
|
const stored = localStorage.getItem('roles')
|
||||||
|
const roles = stored ? JSON.parse(stored) : []
|
||||||
|
|
||||||
|
localStorage.setItem('permission', JSON.stringify({ permissions, menus }))
|
||||||
|
|
||||||
|
set({ menus, permissions, roles, loaded: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取菜单失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission: (permission: string) => {
|
||||||
|
const { permissions } = get()
|
||||||
|
return permissions.includes(permission) || permissions.includes('*')
|
||||||
|
},
|
||||||
|
|
||||||
|
hasRole: (role: string) => {
|
||||||
|
const { roles } = get()
|
||||||
|
return roles.includes(role) || roles.includes('admin')
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPermissionData: () => {
|
||||||
|
localStorage.removeItem('permission')
|
||||||
|
set({ roles: [], permissions: [], menus: [], loaded: false })
|
||||||
|
},
|
||||||
|
|
||||||
|
initFromStorage: () => {
|
||||||
|
const stored = localStorage.getItem('permission')
|
||||||
|
if (!stored) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(stored)
|
||||||
|
const rolesStr = localStorage.getItem('roles')
|
||||||
|
const roles = rolesStr ? JSON.parse(rolesStr) : []
|
||||||
|
|
||||||
|
set({
|
||||||
|
permissions: data.permissions || [],
|
||||||
|
menus: data.menus || [],
|
||||||
|
roles,
|
||||||
|
loaded: true,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
function extractPermissions(menus: MenuItem[]): string[] {
|
||||||
|
const permissions: string[] = []
|
||||||
|
|
||||||
|
function traverse(items: MenuItem[]) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.permission) {
|
||||||
|
permissions.push(item.permission)
|
||||||
|
}
|
||||||
|
if (item.children?.length) {
|
||||||
|
traverse(item.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(menus)
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user