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