后台侧栏按业务分组支持折叠
Project CI / AI game creator shell Rust crates (push) Successful in 2m45s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m17s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m27s
Project CI / Frontend tests (push) Successful in 3m4s
Project CI / Backend tests (push) Successful in 6m32s
Project CI / Native shell tests (push) Successful in 7m36s
Project CI / AI game creator shell web tests (push) Successful in 3m24s
Project CI / Repository checks (push) Successful in 6m44s
Project CI / AI game creator shell Rust crates (push) Successful in 2m45s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m17s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m27s
Project CI / Frontend tests (push) Successful in 3m4s
Project CI / Backend tests (push) Successful in 6m32s
Project CI / Native shell tests (push) Successful in 7m36s
Project CI / AI game creator shell web tests (push) Successful in 3m24s
Project CI / Repository checks (push) Successful in 6m44s
新增后台侧栏业务分组与当前页自动展开行为 补充折叠交互、路由分组和权限过滤测试 同步后台导航分组行为文档
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
Activity,
|
||||
BadgeDollarSign,
|
||||
Bug,
|
||||
ChevronDown,
|
||||
Coins,
|
||||
Database,
|
||||
FolderArchive,
|
||||
@@ -22,10 +23,15 @@ import {
|
||||
Users,
|
||||
WalletCards,
|
||||
} from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { type ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import type { AdminSessionPayload } from '../api/adminApiTypes';
|
||||
import type { AdminRouteDefinition, AdminRouteId } from './adminRoutes';
|
||||
import {
|
||||
type AdminNavGroupId,
|
||||
type AdminRouteDefinition,
|
||||
type AdminRouteId,
|
||||
getAdminNavGroups,
|
||||
} from './adminRoutes';
|
||||
|
||||
interface AdminShellProps {
|
||||
admin: AdminSessionPayload;
|
||||
@@ -72,6 +78,67 @@ export function AdminShell({
|
||||
onRouteChange,
|
||||
onLogout,
|
||||
}: AdminShellProps) {
|
||||
const navGroups = getAdminNavGroups(routes);
|
||||
const activeGroupId =
|
||||
navGroups.find((group) =>
|
||||
group.routes.some((route) => route.id === routeId),
|
||||
)?.id ?? navGroups[0]?.id;
|
||||
const [collapsedGroupIds, setCollapsedGroupIds] = useState<
|
||||
Set<AdminNavGroupId>
|
||||
>(() => {
|
||||
return new Set(
|
||||
navGroups
|
||||
.map((group) => group.id)
|
||||
.filter((groupId) => groupId !== activeGroupId),
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCollapsedGroupIds((current) => {
|
||||
if (!current.has(activeGroupId)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const next = new Set(current);
|
||||
next.delete(activeGroupId);
|
||||
return next;
|
||||
});
|
||||
}, [activeGroupId, routeId]);
|
||||
|
||||
const toggleGroup = (groupId: AdminNavGroupId) => {
|
||||
setCollapsedGroupIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(groupId)) {
|
||||
next.delete(groupId);
|
||||
} else {
|
||||
next.add(groupId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const renderRouteButton = (route: AdminRouteDefinition) => {
|
||||
const Icon = routeIcons[route.id];
|
||||
return (
|
||||
<button
|
||||
className="admin-nav-button"
|
||||
aria-current={route.id === routeId ? 'page' : undefined}
|
||||
data-active={route.id === routeId}
|
||||
key={route.id}
|
||||
title={route.label}
|
||||
type="button"
|
||||
onClick={() => onRouteChange(route.id)}
|
||||
>
|
||||
<Icon size={18} aria-hidden="true" />
|
||||
<span>{route.label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-shell">
|
||||
<aside className="admin-sidebar">
|
||||
@@ -86,20 +153,35 @@ export function AdminShell({
|
||||
</div>
|
||||
|
||||
<nav className="admin-nav" aria-label="后台导航">
|
||||
{routes.map((route) => {
|
||||
const Icon = routeIcons[route.id];
|
||||
{navGroups.map((group) => {
|
||||
const isCollapsed = collapsedGroupIds.has(group.id);
|
||||
const groupItemsId = `admin-nav-group-${group.id}`;
|
||||
return (
|
||||
<button
|
||||
className="admin-nav-button"
|
||||
data-active={route.id === routeId}
|
||||
key={route.id}
|
||||
title={route.label}
|
||||
type="button"
|
||||
onClick={() => onRouteChange(route.id)}
|
||||
<div
|
||||
className="admin-nav-group"
|
||||
data-collapsed={isCollapsed}
|
||||
key={group.id}
|
||||
>
|
||||
<Icon size={18} aria-hidden="true" />
|
||||
<span>{route.label}</span>
|
||||
</button>
|
||||
<button
|
||||
aria-controls={groupItemsId}
|
||||
aria-expanded={!isCollapsed}
|
||||
className="admin-nav-group-toggle"
|
||||
type="button"
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
<ChevronDown size={15} aria-hidden="true" />
|
||||
<span>{group.label}</span>
|
||||
<small aria-hidden="true">{group.routes.length}</small>
|
||||
</button>
|
||||
<div
|
||||
className="admin-nav-group-items"
|
||||
data-collapsed={isCollapsed}
|
||||
hidden={isCollapsed}
|
||||
id={groupItemsId}
|
||||
>
|
||||
{group.routes.map(renderRouteButton)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user