Improve admin loading and menu interactions
This commit is contained in:
parent
e3337f26a6
commit
db964d7471
83
src/App.jsx
83
src/App.jsx
@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { getMenus } from "./api/navigation.js";
|
import { getMenus } from "./api/navigation.js";
|
||||||
import { useAuth } from "./auth/AuthProvider.jsx";
|
import { useAuth } from "./auth/AuthProvider.jsx";
|
||||||
@ -7,47 +7,72 @@ import { PageSkeleton } from "./components/layout/PageSkeleton.jsx";
|
|||||||
import { Sidebar } from "./components/layout/Sidebar.jsx";
|
import { Sidebar } from "./components/layout/Sidebar.jsx";
|
||||||
import { findNavItemByPath, mapBackendMenus, navItems } from "./constants/navigation.js";
|
import { findNavItemByPath, mapBackendMenus, navItems } from "./constants/navigation.js";
|
||||||
|
|
||||||
const LoginPage = lazy(() => import("./pages/LoginPage.jsx").then((module) => ({ default: module.LoginPage })));
|
const pageModules = {
|
||||||
const OverviewPage = lazy(() => import("./pages/OverviewPage.jsx").then((module) => ({ default: module.OverviewPage })));
|
login: () => import("./pages/LoginPage.jsx").then((module) => module.LoginPage),
|
||||||
const ServicesPage = lazy(() => import("./pages/ServicesPage.jsx").then((module) => ({ default: module.ServicesPage })));
|
overview: () => import("./pages/OverviewPage.jsx").then((module) => module.OverviewPage),
|
||||||
const UserManagementPage = lazy(() =>
|
services: () => import("./pages/ServicesPage.jsx").then((module) => module.ServicesPage),
|
||||||
import("./pages/UserManagementPage.jsx").then((module) => ({ default: module.UserManagementPage }))
|
users: () => import("./pages/UserManagementPage.jsx").then((module) => module.UserManagementPage),
|
||||||
);
|
roles: () => import("./pages/RoleManagementPage.jsx").then((module) => module.RoleManagementPage),
|
||||||
const RoleManagementPage = lazy(() =>
|
menus: () => import("./pages/MenuManagementPage.jsx").then((module) => module.MenuManagementPage),
|
||||||
import("./pages/RoleManagementPage.jsx").then((module) => ({ default: module.RoleManagementPage }))
|
logs: () => import("./pages/LogsPage.jsx").then((module) => module.LogsPage),
|
||||||
);
|
notifications: () => import("./pages/NotificationsPage.jsx").then((module) => module.NotificationsPage)
|
||||||
const MenuManagementPage = lazy(() =>
|
};
|
||||||
import("./pages/MenuManagementPage.jsx").then((module) => ({ default: module.MenuManagementPage }))
|
|
||||||
);
|
const pageCache = new Map();
|
||||||
const LogsPage = lazy(() => import("./pages/LogsPage.jsx").then((module) => ({ default: module.LogsPage })));
|
|
||||||
const NotificationsPage = lazy(() =>
|
|
||||||
import("./pages/NotificationsPage.jsx").then((module) => ({ default: module.NotificationsPage }))
|
|
||||||
);
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PageSkeleton />}>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<DeferredPage pageKey="login" />} />
|
||||||
<Route element={<RequireAuth />}>
|
<Route element={<RequireAuth />}>
|
||||||
<Route element={<AdminLayout />}>
|
<Route element={<AdminLayout />}>
|
||||||
<Route index element={<Navigate to="/overview" replace />} />
|
<Route index element={<Navigate to="/overview" replace />} />
|
||||||
<Route path="/overview" element={<RequirePermission code="overview:view"><OverviewPage /></RequirePermission>} />
|
<Route path="/overview" element={<RequirePermission code="overview:view"><DeferredPage pageKey="overview" /></RequirePermission>} />
|
||||||
<Route path="/services" element={<RequirePermission code="service:view"><ServicesPage /></RequirePermission>} />
|
<Route path="/services" element={<RequirePermission code="service:view"><DeferredPage pageKey="services" /></RequirePermission>} />
|
||||||
<Route path="/system/users" element={<RequirePermission code="user:view"><UserManagementPage /></RequirePermission>} />
|
<Route path="/system/users" element={<RequirePermission code="user:view"><DeferredPage pageKey="users" /></RequirePermission>} />
|
||||||
<Route path="/system/roles" element={<RequirePermission code="role:view"><RoleManagementPage /></RequirePermission>} />
|
<Route path="/system/roles" element={<RequirePermission code="role:view"><DeferredPage pageKey="roles" /></RequirePermission>} />
|
||||||
<Route path="/system/menus" element={<RequirePermission code="menu:view"><MenuManagementPage /></RequirePermission>} />
|
<Route path="/system/menus" element={<RequirePermission code="menu:view"><DeferredPage pageKey="menus" /></RequirePermission>} />
|
||||||
<Route path="/logs/login" element={<RequirePermission code="log:view"><LogsPage type="login" /></RequirePermission>} />
|
<Route path="/logs/login" element={<RequirePermission code="log:view"><DeferredPage pageKey="logs" type="login" /></RequirePermission>} />
|
||||||
<Route path="/logs/operations" element={<RequirePermission code="log:view"><LogsPage type="operations" /></RequirePermission>} />
|
<Route path="/logs/operations" element={<RequirePermission code="log:view"><DeferredPage pageKey="logs" type="operations" /></RequirePermission>} />
|
||||||
<Route path="/notifications" element={<RequirePermission code="notification:view"><NotificationsPage /></RequirePermission>} />
|
<Route path="/notifications" element={<RequirePermission code="notification:view"><DeferredPage pageKey="notifications" /></RequirePermission>} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/overview" replace />} />
|
<Route path="*" element={<Navigate to="/overview" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DeferredPage({ pageKey, ...props }) {
|
||||||
|
const [Page, setPage] = useState(() => pageCache.get(pageKey) || null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
if (pageCache.has(pageKey)) {
|
||||||
|
setPage(() => pageCache.get(pageKey));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPage(null);
|
||||||
|
pageModules[pageKey]().then((component) => {
|
||||||
|
pageCache.set(pageKey, component);
|
||||||
|
if (mounted) {
|
||||||
|
setPage(() => component);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [pageKey]);
|
||||||
|
|
||||||
|
if (!Page) {
|
||||||
|
return <PageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Page {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
function RequireAuth() {
|
function RequireAuth() {
|
||||||
const { loading, user } = useAuth();
|
const { loading, user } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -118,11 +143,9 @@ function AdminLayout() {
|
|||||||
<Sidebar activePath={location.pathname} isCollapsed={sidebarCollapsed} items={menus} onNavigate={navigate} />
|
<Sidebar activePath={location.pathname} isCollapsed={sidebarCollapsed} items={menus} onNavigate={navigate} />
|
||||||
|
|
||||||
<main className="workspace">
|
<main className="workspace">
|
||||||
<Suspense fallback={<PageSkeleton />}>
|
|
||||||
<div className="page-transition" key={location.pathname}>
|
<div className="page-transition" key={location.pathname}>
|
||||||
<Outlet context={{ refreshTick }} />
|
<Outlet context={{ refreshTick }} />
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -125,8 +125,9 @@ function NavGroup({
|
|||||||
{hasChildren ? <ExpandMore className="nav-expand" fontSize="small" /> : null}
|
{hasChildren ? <ExpandMore className="nav-expand" fontSize="small" /> : null}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{hasChildren && isOpen && !isCollapsed ? (
|
{hasChildren && !isCollapsed ? (
|
||||||
<div className="nav-children">
|
<div className={`nav-children ${isOpen ? "nav-children--open" : ""}`}>
|
||||||
|
<div className="nav-children__inner">
|
||||||
{children.map((child) => {
|
{children.map((child) => {
|
||||||
const ChildIcon = child.icon;
|
const ChildIcon = child.icon;
|
||||||
|
|
||||||
@ -143,6 +144,7 @@ function NavGroup({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{hasChildren && isCollapsed && isFlyoutOpen ? (
|
{hasChildren && isCollapsed && isFlyoutOpen ? (
|
||||||
|
|||||||
@ -322,7 +322,7 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Switch checked={form.mfaEnabled} onChange={(event) => setForm({ ...form, mfaEnabled: event.target.checked })} />}
|
control={<Switch checked={form.mfaEnabled} onChange={(event) => setForm({ ...form, mfaEnabled: event.target.checked })} />}
|
||||||
label="MFA 已开启"
|
label={form.mfaEnabled ? "MFA 已开启" : "MFA 未开启"}
|
||||||
/>
|
/>
|
||||||
<div className="form-drawer__actions">
|
<div className="form-drawer__actions">
|
||||||
<Button onClick={() => setFormOpen(false)}>取消</Button>
|
<Button onClick={() => setFormOpen(false)}>取消</Button>
|
||||||
|
|||||||
@ -374,13 +374,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell--sidebar-collapsed .brand {
|
.app-shell--sidebar-collapsed .brand {
|
||||||
|
gap: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-text,
|
||||||
|
.nav-label {
|
||||||
|
display: inline-grid;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 160px;
|
||||||
|
opacity: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition:
|
||||||
|
max-width var(--motion-slow) var(--ease-emphasized),
|
||||||
|
opacity var(--motion-base) var(--ease-standard),
|
||||||
|
transform var(--motion-base) var(--ease-emphasized);
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell--sidebar-collapsed .brand-text,
|
.app-shell--sidebar-collapsed .brand-text,
|
||||||
.sidebar--collapsed .nav-label {
|
.sidebar--collapsed .nav-label {
|
||||||
display: none;
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--collapsed {
|
.sidebar--collapsed {
|
||||||
@ -421,6 +439,9 @@
|
|||||||
transition:
|
transition:
|
||||||
background var(--motion-base) var(--ease-standard),
|
background var(--motion-base) var(--ease-standard),
|
||||||
color var(--motion-base) var(--ease-standard),
|
color var(--motion-base) var(--ease-standard),
|
||||||
|
gap var(--motion-slow) var(--ease-emphasized),
|
||||||
|
padding var(--motion-slow) var(--ease-emphasized),
|
||||||
|
width var(--motion-slow) var(--ease-emphasized),
|
||||||
transform var(--motion-base) var(--ease-emphasized);
|
transform var(--motion-base) var(--ease-emphasized);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,8 +473,30 @@
|
|||||||
|
|
||||||
.nav-children {
|
.nav-children {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
transition:
|
||||||
|
grid-template-rows var(--motion-slow) var(--ease-emphasized),
|
||||||
|
opacity var(--motion-base) var(--ease-standard),
|
||||||
|
transform var(--motion-slow) var(--ease-emphasized);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-children--open {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-children__inner {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item--child {
|
.nav-item--child {
|
||||||
|
|||||||
53
src/theme.js
53
src/theme.js
@ -144,6 +144,59 @@ export const theme = createTheme({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
MuiSwitch: {
|
||||||
|
defaultProps: {
|
||||||
|
disableRipple: true
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
width: 46,
|
||||||
|
height: 26,
|
||||||
|
padding: 0,
|
||||||
|
margin: "0 8px",
|
||||||
|
overflow: "visible"
|
||||||
|
},
|
||||||
|
switchBase: {
|
||||||
|
padding: 3,
|
||||||
|
color: "#ffffff",
|
||||||
|
transition: "transform 220ms cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "#ffffff",
|
||||||
|
transform: "translateX(20px)",
|
||||||
|
"& + .MuiSwitch-track": {
|
||||||
|
borderColor: "rgba(37, 99, 235, 0.36)",
|
||||||
|
background: "linear-gradient(135deg, #2563eb, #0ea5e9)",
|
||||||
|
boxShadow: "0 8px 20px rgba(37, 99, 235, 0.22)",
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
"& .MuiSwitch-thumb": {
|
||||||
|
boxShadow: "0 4px 12px rgba(15, 23, 42, 0.18)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "#ffffff",
|
||||||
|
"& + .MuiSwitch-track": {
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
thumb: {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
boxShadow: "0 3px 10px rgba(15, 23, 42, 0.18)"
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
border: "1px solid #d8e0ec",
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "#e2e8f0",
|
||||||
|
boxShadow: "inset 0 1px 2px rgba(15, 23, 42, 0.06)",
|
||||||
|
opacity: 1,
|
||||||
|
transition:
|
||||||
|
"background 220ms cubic-bezier(0.2, 0, 0, 1), border-color 220ms cubic-bezier(0.2, 0, 0, 1), box-shadow 220ms cubic-bezier(0.2, 0, 0, 1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiOutlinedInput: {
|
MuiOutlinedInput: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
notchedOutline: {
|
notchedOutline: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user