Improve admin loading and menu interactions
This commit is contained in:
parent
e3337f26a6
commit
db964d7471
103
src/App.jsx
103
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 { getMenus } from "./api/navigation.js";
|
||||
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 { findNavItemByPath, mapBackendMenus, navItems } from "./constants/navigation.js";
|
||||
|
||||
const LoginPage = lazy(() => import("./pages/LoginPage.jsx").then((module) => ({ default: module.LoginPage })));
|
||||
const OverviewPage = lazy(() => import("./pages/OverviewPage.jsx").then((module) => ({ default: module.OverviewPage })));
|
||||
const ServicesPage = lazy(() => import("./pages/ServicesPage.jsx").then((module) => ({ default: module.ServicesPage })));
|
||||
const UserManagementPage = lazy(() =>
|
||||
import("./pages/UserManagementPage.jsx").then((module) => ({ default: module.UserManagementPage }))
|
||||
);
|
||||
const RoleManagementPage = lazy(() =>
|
||||
import("./pages/RoleManagementPage.jsx").then((module) => ({ default: module.RoleManagementPage }))
|
||||
);
|
||||
const MenuManagementPage = lazy(() =>
|
||||
import("./pages/MenuManagementPage.jsx").then((module) => ({ default: module.MenuManagementPage }))
|
||||
);
|
||||
const LogsPage = lazy(() => import("./pages/LogsPage.jsx").then((module) => ({ default: module.LogsPage })));
|
||||
const NotificationsPage = lazy(() =>
|
||||
import("./pages/NotificationsPage.jsx").then((module) => ({ default: module.NotificationsPage }))
|
||||
);
|
||||
const pageModules = {
|
||||
login: () => import("./pages/LoginPage.jsx").then((module) => module.LoginPage),
|
||||
overview: () => import("./pages/OverviewPage.jsx").then((module) => module.OverviewPage),
|
||||
services: () => import("./pages/ServicesPage.jsx").then((module) => module.ServicesPage),
|
||||
users: () => import("./pages/UserManagementPage.jsx").then((module) => module.UserManagementPage),
|
||||
roles: () => import("./pages/RoleManagementPage.jsx").then((module) => module.RoleManagementPage),
|
||||
menus: () => import("./pages/MenuManagementPage.jsx").then((module) => module.MenuManagementPage),
|
||||
logs: () => import("./pages/LogsPage.jsx").then((module) => module.LogsPage),
|
||||
notifications: () => import("./pages/NotificationsPage.jsx").then((module) => module.NotificationsPage)
|
||||
};
|
||||
|
||||
const pageCache = new Map();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="/overview" replace />} />
|
||||
<Route path="/overview" element={<RequirePermission code="overview:view"><OverviewPage /></RequirePermission>} />
|
||||
<Route path="/services" element={<RequirePermission code="service:view"><ServicesPage /></RequirePermission>} />
|
||||
<Route path="/system/users" element={<RequirePermission code="user:view"><UserManagementPage /></RequirePermission>} />
|
||||
<Route path="/system/roles" element={<RequirePermission code="role:view"><RoleManagementPage /></RequirePermission>} />
|
||||
<Route path="/system/menus" element={<RequirePermission code="menu:view"><MenuManagementPage /></RequirePermission>} />
|
||||
<Route path="/logs/login" element={<RequirePermission code="log:view"><LogsPage type="login" /></RequirePermission>} />
|
||||
<Route path="/logs/operations" element={<RequirePermission code="log:view"><LogsPage type="operations" /></RequirePermission>} />
|
||||
<Route path="/notifications" element={<RequirePermission code="notification:view"><NotificationsPage /></RequirePermission>} />
|
||||
</Route>
|
||||
<Routes>
|
||||
<Route path="/login" element={<DeferredPage pageKey="login" />} />
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="/overview" replace />} />
|
||||
<Route path="/overview" element={<RequirePermission code="overview:view"><DeferredPage pageKey="overview" /></RequirePermission>} />
|
||||
<Route path="/services" element={<RequirePermission code="service:view"><DeferredPage pageKey="services" /></RequirePermission>} />
|
||||
<Route path="/system/users" element={<RequirePermission code="user:view"><DeferredPage pageKey="users" /></RequirePermission>} />
|
||||
<Route path="/system/roles" element={<RequirePermission code="role:view"><DeferredPage pageKey="roles" /></RequirePermission>} />
|
||||
<Route path="/system/menus" element={<RequirePermission code="menu:view"><DeferredPage pageKey="menus" /></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"><DeferredPage pageKey="logs" type="operations" /></RequirePermission>} />
|
||||
<Route path="/notifications" element={<RequirePermission code="notification:view"><DeferredPage pageKey="notifications" /></RequirePermission>} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/overview" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/overview" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
const { loading, user } = useAuth();
|
||||
const location = useLocation();
|
||||
@ -118,11 +143,9 @@ function AdminLayout() {
|
||||
<Sidebar activePath={location.pathname} isCollapsed={sidebarCollapsed} items={menus} onNavigate={navigate} />
|
||||
|
||||
<main className="workspace">
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<div className="page-transition" key={location.pathname}>
|
||||
<Outlet context={{ refreshTick }} />
|
||||
</div>
|
||||
</Suspense>
|
||||
<div className="page-transition" key={location.pathname}>
|
||||
<Outlet context={{ refreshTick }} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -125,23 +125,25 @@ function NavGroup({
|
||||
{hasChildren ? <ExpandMore className="nav-expand" fontSize="small" /> : null}
|
||||
</button>
|
||||
|
||||
{hasChildren && isOpen && !isCollapsed ? (
|
||||
<div className="nav-children">
|
||||
{children.map((child) => {
|
||||
const ChildIcon = child.icon;
|
||||
{hasChildren && !isCollapsed ? (
|
||||
<div className={`nav-children ${isOpen ? "nav-children--open" : ""}`}>
|
||||
<div className="nav-children__inner">
|
||||
{children.map((child) => {
|
||||
const ChildIcon = child.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`nav-item nav-item--child ${child.path && activePath.startsWith(child.path) ? "nav-item--active" : ""}`}
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate(child.path)}
|
||||
>
|
||||
<ChildIcon fontSize="small" />
|
||||
<span className="nav-label">{child.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<button
|
||||
className={`nav-item nav-item--child ${child.path && activePath.startsWith(child.path) ? "nav-item--active" : ""}`}
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate(child.path)}
|
||||
>
|
||||
<ChildIcon fontSize="small" />
|
||||
<span className="nav-label">{child.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@ -322,7 +322,7 @@ export function UserManagementPage() {
|
||||
</div>
|
||||
<FormControlLabel
|
||||
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">
|
||||
<Button onClick={() => setFormOpen(false)}>取消</Button>
|
||||
|
||||
@ -374,13 +374,31 @@
|
||||
}
|
||||
|
||||
.app-shell--sidebar-collapsed .brand {
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
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,
|
||||
.sidebar--collapsed .nav-label {
|
||||
display: none;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
@ -421,6 +439,9 @@
|
||||
transition:
|
||||
background 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);
|
||||
}
|
||||
|
||||
@ -452,8 +473,30 @@
|
||||
|
||||
.nav-children {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
|
||||
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: {
|
||||
styleOverrides: {
|
||||
notchedOutline: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user