mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-12 11:32:56 +08:00
feat(adminLayout): refactor route matching logic and improve breadcrumb handling
This commit is contained in:
@@ -81,7 +81,7 @@ function App() {
|
||||
{adminRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.key}
|
||||
path={route.path}
|
||||
path={route.path === '' ? 'index' : route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -27,80 +27,63 @@ function AdminLayout() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const routes = useMemo(() => getAdminRoutes(), []);
|
||||
|
||||
const headerRouteData = useMemo(() => ({
|
||||
routeInfo: currentRouteData.routeInfo,
|
||||
params: currentRouteData.params,
|
||||
title: (currentRouteData.routeInfo?.label || '')
|
||||
}), [currentRouteData]);
|
||||
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
const { token: { colorBgContainer } } = theme.useToken();
|
||||
|
||||
const findCurrentRoute = useCallback(() => {
|
||||
const pathname = location.pathname;
|
||||
const adminPath = pathname.replace(/^\/admin\/?/, '');
|
||||
const adminBasePrefix = '/admin';
|
||||
|
||||
if (!pathname.startsWith(adminBasePrefix)) {
|
||||
return { routeInfo: undefined, params: {} };
|
||||
}
|
||||
|
||||
let adminPath = pathname.substring(adminBasePrefix.length);
|
||||
if (adminPath.startsWith('/')) {
|
||||
adminPath = adminPath.substring(1);
|
||||
}
|
||||
|
||||
if (adminPath.length > 0 && adminPath.endsWith('/')) {
|
||||
adminPath = adminPath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (adminPath === '') {
|
||||
const defaultRoute = routes.find(route => route.path === '');
|
||||
if (defaultRoute) {
|
||||
return {
|
||||
routeInfo: defaultRoute,
|
||||
params: {}
|
||||
};
|
||||
return { routeInfo: defaultRoute, params: {} };
|
||||
}
|
||||
}
|
||||
|
||||
// 查找精确匹配的路由
|
||||
for (const route of routes) {
|
||||
const match = matchPath(
|
||||
{ path: route.path, end: true },
|
||||
adminPath
|
||||
);
|
||||
if (route.path === '' && adminPath !== '') continue;
|
||||
if (route.path !== '' && adminPath === '') continue;
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
routeInfo: route,
|
||||
params: Object.fromEntries(
|
||||
Object.entries(match.params || {}).filter(
|
||||
([, value]) => value !== undefined
|
||||
)
|
||||
) as Record<string, string>
|
||||
};
|
||||
if (route.path === adminPath) {
|
||||
return { routeInfo: route, params: {} };
|
||||
}
|
||||
}
|
||||
|
||||
// 查找包含参数的路由
|
||||
for (const route of routes) {
|
||||
if (route.path.includes(':')) {
|
||||
const basePath = route.path.split('/:')[0];
|
||||
if (adminPath.startsWith(basePath)) {
|
||||
const match = matchPath(
|
||||
{ path: route.path, end: false },
|
||||
adminPath
|
||||
);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
routeInfo: route,
|
||||
params: Object.fromEntries(
|
||||
Object.entries(match.params || {}).filter(
|
||||
([, value]) => value !== undefined
|
||||
)
|
||||
) as Record<string, string>
|
||||
};
|
||||
}
|
||||
const match = matchPath({ path: route.path, end: true }, adminPath);
|
||||
if (match) {
|
||||
return {
|
||||
routeInfo: route,
|
||||
params: Object.fromEntries(
|
||||
Object.entries(match.params || {}).filter(
|
||||
([, value]) => value !== undefined && value !== ""
|
||||
).map(([key, value]) => [key, String(value)])
|
||||
) as Record<string, string>
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
routeInfo: undefined,
|
||||
params: {}
|
||||
};
|
||||
return { routeInfo: undefined, params: {} };
|
||||
}, [location.pathname, routes]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -108,7 +91,6 @@ function AdminLayout() {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
refreshUser();
|
||||
}
|
||||
@@ -120,15 +102,16 @@ function AdminLayout() {
|
||||
navigate('/');
|
||||
}
|
||||
}, [user, hasRole, navigate, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
const routeData = findCurrentRoute();
|
||||
setCurrentRouteData(routeData);
|
||||
}, [location.pathname, findCurrentRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
setCollapsed(isMobile);
|
||||
}, [isMobile]);
|
||||
|
||||
// 退出登录处理
|
||||
const handleLogout = () => {
|
||||
clearAuthData();
|
||||
navigate('/login');
|
||||
@@ -138,34 +121,23 @@ function AdminLayout() {
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
加载中...
|
||||
</div>;
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
加载中...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (user && !hasRole(UserRole.Administrator)) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout style={{
|
||||
height: '100vh',
|
||||
background: '#f0f2f5',
|
||||
fontWeight: 400
|
||||
}}>
|
||||
{/* 侧边栏组件 */}
|
||||
<Sidebar
|
||||
collapsed={collapsed}
|
||||
isMobile={isMobile}
|
||||
onClose={toggleCollapsed}
|
||||
area="admin"
|
||||
/>
|
||||
|
||||
<Layout style={{ height: '100vh', background: '#f0f2f5', fontWeight: 400 }}>
|
||||
<Sidebar collapsed={collapsed} isMobile={isMobile} onClose={toggleCollapsed} area="admin" />
|
||||
|
||||
<Layout>
|
||||
{/* 顶部导航栏组件 */}
|
||||
<Header
|
||||
collapsed={collapsed}
|
||||
toggleCollapsed={toggleCollapsed}
|
||||
@@ -174,7 +146,6 @@ function AdminLayout() {
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
{/* 主要内容区 */}
|
||||
<Content style={{
|
||||
margin: isMobile ? '10px' : '20px',
|
||||
background: '#f0f2f5',
|
||||
@@ -191,14 +162,10 @@ function AdminLayout() {
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 渲染子路由组件 */}
|
||||
<Outlet context={{
|
||||
isMobile,
|
||||
isAdminPanel: true
|
||||
}} />
|
||||
<Outlet context={{ isMobile, isAdminPanel: true }} />
|
||||
</div>
|
||||
</Content>
|
||||
{/* 页脚组件 */}
|
||||
|
||||
<Footer isMobile={isMobile} />
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import {Outlet, useNavigate, useLocation, matchPath} from 'react-router';
|
||||
import {Layout, theme} from 'antd';
|
||||
import {clearAuthData, isAuthenticated} from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Outlet, useNavigate, useLocation, matchPath } from 'react-router';
|
||||
import { Layout, theme } from 'antd';
|
||||
import { clearAuthData, isAuthenticated } from '../api';
|
||||
import useIsMobile from '../hooks/useIsMobile';
|
||||
import {useAuth} from '../auth/AuthContext';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
import {getMainRoutes, type RouteConfig} from '../routes';
|
||||
import { getMainRoutes, type RouteConfig } from '../routes';
|
||||
|
||||
const {Content} = Layout;
|
||||
const { Content } = Layout;
|
||||
|
||||
function MainLayout() {
|
||||
const {refreshUser} = useAuth();
|
||||
const { refreshUser } = useAuth();
|
||||
const isMobile = useIsMobile();
|
||||
const [collapsed, setCollapsed] = useState(isMobile);
|
||||
const [currentRouteData, setCurrentRouteData] = useState<{
|
||||
@@ -29,7 +29,7 @@ function MainLayout() {
|
||||
const routes = getMainRoutes();
|
||||
|
||||
const {
|
||||
token: {colorBgContainer},
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
|
||||
// 查找当前路由信息
|
||||
@@ -39,7 +39,7 @@ function MainLayout() {
|
||||
// 测试每个路由是否匹配当前路径
|
||||
for (const route of routes) {
|
||||
const match = matchPath(
|
||||
{path: `/${route.path}`, end: true},
|
||||
{ path: `/${route.path}`, end: true },
|
||||
pathname
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ function MainLayout() {
|
||||
|
||||
if (pathname.startsWith(pattern)) {
|
||||
const match = matchPath(
|
||||
{path: `/${route.path}`, end: false},
|
||||
{ path: `/${route.path}`, end: false },
|
||||
pathname
|
||||
);
|
||||
|
||||
@@ -141,29 +141,24 @@ function MainLayout() {
|
||||
|
||||
{/* 主要内容区 */}
|
||||
<Content style={{
|
||||
margin: isMobile ? '10px' : '20px',
|
||||
background: '#fcfcfc',
|
||||
padding: isMobile ? '10px' : '20px',
|
||||
background: colorBgContainer,
|
||||
position: 'relative',
|
||||
borderRadius: isMobile ? 10 : 20,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: isMobile ? '15px' : '25px',
|
||||
minHeight: '100%',
|
||||
background: colorBgContainer,
|
||||
boxShadow: '0 6px 30px rgba(0,0,0,0.03)',
|
||||
border: '1px solid #f0f0f0',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 渲染子路由组件 */}
|
||||
<Outlet context={{
|
||||
isMobile
|
||||
}}/>
|
||||
}} />
|
||||
</div>
|
||||
</Content>
|
||||
{/* 页脚组件 */}
|
||||
<Footer isMobile={isMobile}/>
|
||||
<Footer isMobile={isMobile} />
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -12,15 +12,15 @@ const Footer: React.FC<FooterProps> = ({ isMobile = false }) => {
|
||||
return (
|
||||
<AntFooter style={{
|
||||
background: 'white',
|
||||
padding: isMobile ? '10px' : '10px',
|
||||
padding: '3px',
|
||||
fontSize: isMobile ? '12px' : '12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>Foxel ©{new Date().getFullYear()}</div>
|
||||
<a
|
||||
href="https://github.com/DrizzleTime/Foxel"
|
||||
<a
|
||||
href="https://github.com/DrizzleTime/Foxel"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontSize: isMobile ? '16px' : '18px', color: '#333' }}
|
||||
|
||||
@@ -6,19 +6,19 @@ import {
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
DashboardOutlined,
|
||||
HomeOutlined,
|
||||
RightOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { Link, useNavigate, useLocation } from 'react-router';
|
||||
import { useAuth } from '../../auth/AuthContext';
|
||||
import { type RouteConfig } from '../../routes';
|
||||
import { getMainRoutes, getAdminRoutes, type RouteConfig } from '../../routes';
|
||||
import UserAvatar from '../../components/UserAvatar';
|
||||
import { UserRole } from '../../api/types';
|
||||
import SearchDialog from '../../components/search/SearchDialog';
|
||||
import useIsMobile from '../../hooks/useIsMobile';
|
||||
|
||||
const { Header: AntHeader } = Layout;
|
||||
|
||||
interface HeaderProps {
|
||||
collapsed: boolean;
|
||||
toggleCollapsed: () => void;
|
||||
@@ -31,13 +31,6 @@ interface HeaderProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
// 面包屑项目类型定义
|
||||
interface BreadcrumbItem {
|
||||
title: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
collapsed,
|
||||
toggleCollapsed,
|
||||
@@ -45,21 +38,16 @@ const Header: React.FC<HeaderProps> = ({
|
||||
currentRouteData,
|
||||
isMobile = false
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const { user, hasRole } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const { hasRole } = useAuth();
|
||||
const isMobileDevice = useIsMobile();
|
||||
|
||||
// 添加搜索对话框状态
|
||||
const [searchDialogVisible, setSearchDialogVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken();
|
||||
const { token: { colorBgContainer } } = theme.useToken();
|
||||
|
||||
// 用户菜单项
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
@@ -72,7 +60,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
key: 'admin',
|
||||
icon: <DashboardOutlined />,
|
||||
label: '后台管理',
|
||||
onClick: () => navigate('/admin')
|
||||
onClick: () => navigate('/admin/dashboard')
|
||||
}
|
||||
] : []),
|
||||
{
|
||||
@@ -83,128 +71,124 @@ const Header: React.FC<HeaderProps> = ({
|
||||
}
|
||||
];
|
||||
|
||||
// 根据路由信息生成面包屑导航
|
||||
const renderBreadcrumb = () => {
|
||||
// 如果有传入的标题,直接使用标题作为面包屑
|
||||
if (currentRouteData.title) {
|
||||
return (
|
||||
<Breadcrumb
|
||||
separator={<RightOutlined style={{ fontSize: 12 }} />}
|
||||
style={{ margin: 0 }}
|
||||
items={[
|
||||
{
|
||||
title: '首页',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
title: currentRouteData.title
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const { routeInfo, params, title: explicitTitle } = currentRouteData;
|
||||
const antdBreadcrumbItems: Array<{ title: React.ReactNode; href?: string }> = [];
|
||||
|
||||
const currentPath = location.pathname;
|
||||
const isAdminArea = routeInfo?.area === 'admin';
|
||||
const baseHref = isAdminArea ? '/admin' : '/';
|
||||
const baseTitle = isAdminArea ? '管理后台' : '首页';
|
||||
|
||||
if (currentPath === baseHref && !explicitTitle && (!routeInfo || routeInfo.path === '')) {
|
||||
antdBreadcrumbItems.push({ title: baseTitle });
|
||||
} else {
|
||||
antdBreadcrumbItems.push({ title: <Link to={baseHref}>{baseTitle}</Link> });
|
||||
}
|
||||
|
||||
// 如果没有路由信息,返回首页面包屑
|
||||
if (!currentRouteData.routeInfo) {
|
||||
return (
|
||||
<Breadcrumb
|
||||
separator={<RightOutlined style={{ fontSize: 12 }} />}
|
||||
style={{ margin: 0 }}
|
||||
items={[
|
||||
{
|
||||
title: '首页',
|
||||
href: '/',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 获取当前路由信息
|
||||
const { routeInfo, params } = currentRouteData;
|
||||
const breadcrumb = routeInfo.breadcrumb;
|
||||
|
||||
if (!breadcrumb) {
|
||||
return (
|
||||
<Breadcrumb
|
||||
separator={<RightOutlined style={{ fontSize: 12 }} />}
|
||||
style={{ margin: 0 }}
|
||||
items={[
|
||||
{
|
||||
title: '首页',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
title: routeInfo.label
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 准备面包屑项目
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
title: routeInfo.area === 'admin' ? '管理后台' : '首页',
|
||||
href: routeInfo.area === 'admin' ? '/admin' : '/',
|
||||
icon: routeInfo.area === 'admin' ? <DashboardOutlined /> : <HomeOutlined />
|
||||
if (explicitTitle) {
|
||||
if (!(antdBreadcrumbItems.length === 1 && !antdBreadcrumbItems[0].href && antdBreadcrumbItems[0].title === explicitTitle)) {
|
||||
if (antdBreadcrumbItems.length === 1 && antdBreadcrumbItems[0].href && antdBreadcrumbItems[0].title !== explicitTitle) {
|
||||
antdBreadcrumbItems.push({ title: explicitTitle });
|
||||
} else if (antdBreadcrumbItems.length === 0 || antdBreadcrumbItems[0].title !== explicitTitle) {
|
||||
antdBreadcrumbItems.push({ title: explicitTitle });
|
||||
} else if (antdBreadcrumbItems.length === 1 && !antdBreadcrumbItems[0].href && antdBreadcrumbItems[0].title !== explicitTitle) {
|
||||
antdBreadcrumbItems[0].title = <Link to={baseHref}>{baseTitle}</Link>;
|
||||
antdBreadcrumbItems.push({ title: explicitTitle });
|
||||
}
|
||||
}
|
||||
];
|
||||
} else if (routeInfo) {
|
||||
const allRoutesForArea = isAdminArea ? getAdminRoutes() : getMainRoutes();
|
||||
const { breadcrumb: breadcrumbConfig, label: routeLabel, path: routeConfigPath } = routeInfo;
|
||||
|
||||
// 如果有父级,添加父级面包屑
|
||||
if (breadcrumb.parent) {
|
||||
const parentPath = routeInfo.area === 'admin'
|
||||
? `/admin/${breadcrumb.parent}`
|
||||
: `/${breadcrumb.parent}`;
|
||||
if (breadcrumbConfig?.parent) {
|
||||
const parentRoute = allRoutesForArea.find(r => r.key === breadcrumbConfig.parent);
|
||||
if (parentRoute) {
|
||||
const parentTitle = parentRoute.breadcrumb?.title || parentRoute.label;
|
||||
let parentHref: string;
|
||||
|
||||
if (isAdminArea) {
|
||||
parentHref = parentRoute.path ? `/admin/${parentRoute.path}` : '/admin';
|
||||
} else {
|
||||
if (parentRoute.path === '') {
|
||||
parentHref = '/';
|
||||
} else if (parentRoute.path.startsWith('/')) {
|
||||
parentHref = parentRoute.path;
|
||||
} else {
|
||||
parentHref = `/${parentRoute.path}`;
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbItems.push({
|
||||
title: breadcrumb.parent.charAt(0).toUpperCase() + breadcrumb.parent.slice(1),
|
||||
href: parentPath
|
||||
});
|
||||
if (parentHref !== baseHref) {
|
||||
if (currentPath === parentHref) {
|
||||
antdBreadcrumbItems.push({ title: parentTitle });
|
||||
} else {
|
||||
antdBreadcrumbItems.push({ title: <Link to={parentHref}>{parentTitle}</Link> });
|
||||
}
|
||||
} else if (antdBreadcrumbItems.length > 0 && antdBreadcrumbItems[0].title !== parentTitle && currentPath !== baseHref) {
|
||||
antdBreadcrumbItems[0].title = <Link to={baseHref}>{parentTitle}</Link>;
|
||||
} else if (antdBreadcrumbItems.length > 0 && antdBreadcrumbItems[0].title !== parentTitle && currentPath === baseHref) {
|
||||
antdBreadcrumbItems[0].title = parentTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentPageTitle = breadcrumbConfig?.title || routeLabel;
|
||||
|
||||
if (breadcrumbConfig?.title && params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
currentPageTitle = currentPageTitle.replace(`:${key}`, String(value));
|
||||
});
|
||||
}
|
||||
|
||||
const lastItem = antdBreadcrumbItems.length > 0 ? antdBreadcrumbItems[antdBreadcrumbItems.length - 1] : null;
|
||||
const lastItemTitle = lastItem ? ((lastItem.title as any)?.props?.children || lastItem.title) : null;
|
||||
|
||||
if (lastItemTitle !== currentPageTitle || lastItem?.href) {
|
||||
if (!(routeConfigPath === '' && antdBreadcrumbItems.length === 1 && !antdBreadcrumbItems[0].href && lastItemTitle === currentPageTitle)) {
|
||||
antdBreadcrumbItems.push({ title: currentPageTitle });
|
||||
} else if (routeConfigPath === '' && antdBreadcrumbItems.length === 1 && !antdBreadcrumbItems[0].href && lastItemTitle !== currentPageTitle) {
|
||||
antdBreadcrumbItems[0].title = currentPageTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueItems = antdBreadcrumbItems.reduce((acc, item) => {
|
||||
if (acc.length === 0) {
|
||||
acc.push(item);
|
||||
} else {
|
||||
const prevItem = acc[acc.length - 1];
|
||||
const prevTitleContent = (prevItem.title as any)?.props?.children ?? prevItem.title;
|
||||
const currentTitleContent = (item.title as any)?.props?.children ?? item.title;
|
||||
|
||||
// 获取动态标题
|
||||
let title = breadcrumb.title;
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
// 用参数替换标题中的占位符,如 ":id"
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
title = title.replace(`:${key}`, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加当前页面面包屑
|
||||
breadcrumbItems.push({
|
||||
title: title
|
||||
});
|
||||
if (prevTitleContent !== currentTitleContent) {
|
||||
acc.push(item);
|
||||
} else {
|
||||
if (!item.href) {
|
||||
acc[acc.length - 1] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<{ title: React.ReactNode; href?: string }>);
|
||||
|
||||
return (
|
||||
<Breadcrumb
|
||||
separator={<RightOutlined style={{ fontSize: 12 }} />}
|
||||
style={{ margin: 0 }}
|
||||
items={breadcrumbItems.map(item => ({
|
||||
title: item.href ? (
|
||||
<Link to={item.href} style={{ color: '#666', fontSize: isMobile ? 13 : 14 }}>
|
||||
{item.icon && <span style={{ marginRight: 4 }}>{item.icon}</span>}
|
||||
{isMobile && !item.icon ? '' : item.title}
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: isMobile ? 14 : 16, fontWeight: 500 }}>
|
||||
{item.icon && <span style={{ marginRight: 4 }}>{item.icon}</span>}
|
||||
{item.title}
|
||||
</span>
|
||||
),
|
||||
items={uniqueItems.map(item => ({
|
||||
title: item.title,
|
||||
href: item.href,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchText(value);
|
||||
setSearchDialogVisible(true);
|
||||
};
|
||||
|
||||
// 关闭搜索对话框
|
||||
const handleSearchDialogClose = () => {
|
||||
setSearchDialogVisible(false);
|
||||
};
|
||||
@@ -225,7 +209,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||
top: 0
|
||||
}}
|
||||
>
|
||||
{/* 左侧区域:折叠按钮和面包屑 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -241,9 +224,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
{!isMobileDevice && renderBreadcrumb()}
|
||||
</div>
|
||||
|
||||
{/* 右侧区域:搜索框和用户菜单 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* 搜索框 */}
|
||||
<div style={{
|
||||
marginRight: 16,
|
||||
display: 'flex',
|
||||
@@ -264,7 +245,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 用户菜单 */}
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
<UserAvatar
|
||||
@@ -276,7 +256,6 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* 搜索对话框 */}
|
||||
<SearchDialog
|
||||
visible={searchDialogVisible}
|
||||
onClose={handleSearchDialogClose}
|
||||
|
||||
@@ -79,7 +79,7 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed, isMobile = false, onClose,
|
||||
// 管理后台路径处理
|
||||
if (area === 'admin') {
|
||||
// 提取 /admin/ 后面的部分
|
||||
const adminPath = pathname.replace(/^\/admin\/?/, '');
|
||||
let adminPath = pathname.replace(/^\/admin\/?/, '');
|
||||
|
||||
// 如果是管理后台首页
|
||||
if (adminPath === '') {
|
||||
@@ -87,18 +87,25 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed, isMobile = false, onClose,
|
||||
return defaultRoute ? defaultRoute.path : '';
|
||||
}
|
||||
|
||||
// 先尝试精确匹配
|
||||
const exactMatch = routes.find(route => route.path === adminPath);
|
||||
if (exactMatch) {
|
||||
return exactMatch.path;
|
||||
}
|
||||
|
||||
// 再尝试参数路由匹配
|
||||
const matchedRoute = routes.find(route => {
|
||||
if (route.path.includes(':')) {
|
||||
const basePath = route.path.split(':')[0].replace(/\/$/, '');
|
||||
return adminPath.startsWith(basePath);
|
||||
}
|
||||
return adminPath === route.path;
|
||||
return false;
|
||||
});
|
||||
|
||||
return matchedRoute ? matchedRoute.path : '';
|
||||
}
|
||||
|
||||
// 主应用路径处理
|
||||
// 主应用路径处理保持不变
|
||||
const matchedRoute = routes.find(route => {
|
||||
if (route.path.includes(':')) {
|
||||
const basePath = route.path.split(':')[0].replace(/\/$/, '');
|
||||
|
||||
@@ -12,7 +12,7 @@ function AllImages() {
|
||||
const isMobile = useIsMobile();
|
||||
const [, setImages] = useState<PictureResponse[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [sortBy, setSortBy] = useState<string>('uploadDate_desc');
|
||||
const [isUploadDialogVisible, setIsUploadDialogVisible] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
@@ -157,7 +157,7 @@ const routes: RouteConfig[] = [
|
||||
hideInMenu: true,
|
||||
breadcrumb: {
|
||||
title: '用户详情',
|
||||
parent: 'users'
|
||||
parent: 'admin-user' // 修改: 指向父路由的 key
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user