mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-05 15:39:45 +08:00
refactor: restructure directories to improve module organization Foxel.Models.Request.Picture - Foxel.Models.Request.Tag - Foxel.Models.Request.Auth - Foxel.Models.Request.Picture
This commit is contained in:
34
Web/src/layouts/components/Footer.tsx
Normal file
34
Web/src/layouts/components/Footer.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import { GithubOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Footer: AntFooter } = Layout;
|
||||
|
||||
interface FooterProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ isMobile = false }) => {
|
||||
return (
|
||||
<AntFooter style={{
|
||||
background: 'white',
|
||||
padding: isMobile ? '10px' : '10px',
|
||||
fontSize: isMobile ? '12px' : '12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>Foxel ©{new Date().getFullYear()}</div>
|
||||
<a
|
||||
href="https://github.com/DrizzleTime/Foxel"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontSize: isMobile ? '16px' : '18px', color: '#333' }}
|
||||
>
|
||||
<GithubOutlined />
|
||||
</a>
|
||||
</AntFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
193
Web/src/layouts/components/Header.tsx
Normal file
193
Web/src/layouts/components/Header.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Layout, Button, Dropdown, Breadcrumb, Input } from 'antd';
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import routes, { type RouteConfig } from '../../config/routeConfig';
|
||||
import UserAvatar from '../../components/UserAvatar';
|
||||
import { useAuth } from '../../api/AuthContext';
|
||||
import { useState } from 'react';
|
||||
import SearchDialog from '../../components/search/SearchDialog';
|
||||
|
||||
const { Header: AntHeader } = Layout;
|
||||
const { Search } = Input;
|
||||
|
||||
interface HeaderProps {
|
||||
collapsed: boolean;
|
||||
toggleCollapsed: () => void;
|
||||
onLogout: () => void;
|
||||
currentRouteData?: {
|
||||
routeInfo: RouteConfig | undefined;
|
||||
params: Record<string, string>;
|
||||
title?: string; // 动态标题,用于显示如"相册名称"等动态数据
|
||||
};
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
collapsed,
|
||||
toggleCollapsed,
|
||||
onLogout,
|
||||
currentRouteData,
|
||||
isMobile = false
|
||||
}: HeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [searchDialogVisible, setSearchDialogVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined/>,
|
||||
label: '个人资料',
|
||||
onClick: () => navigate('/settings')
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: <SettingOutlined/>,
|
||||
label: '设置',
|
||||
onClick: () => navigate('/settings')
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined/>,
|
||||
label: '退出登录',
|
||||
onClick: onLogout
|
||||
}
|
||||
];
|
||||
|
||||
// 生成面包屑项
|
||||
const generateBreadcrumbItems = () => {
|
||||
const breadcrumbItems = [];
|
||||
|
||||
// 添加首页
|
||||
breadcrumbItems.push({
|
||||
key: 'home',
|
||||
title: <Link to="/">首页</Link>,
|
||||
});
|
||||
|
||||
// 确保routeInfo和breadcrumb都存在
|
||||
if (currentRouteData?.routeInfo && currentRouteData.routeInfo.breadcrumb) {
|
||||
const { routeInfo, title } = currentRouteData;
|
||||
const breadcrumb = routeInfo.breadcrumb;
|
||||
|
||||
// 如果有父级路由,先添加父级路由的面包屑
|
||||
if (breadcrumb && breadcrumb.parent) {
|
||||
const parentRoute = routes.find(r => r.key === breadcrumb.parent);
|
||||
if (parentRoute && parentRoute.breadcrumb) {
|
||||
breadcrumbItems.push({
|
||||
key: parentRoute.key,
|
||||
title: <Link to={`/${parentRoute.path}`}>{parentRoute.breadcrumb.title}</Link>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前路由的面包屑
|
||||
breadcrumbItems.push({
|
||||
key: routeInfo.key,
|
||||
title: title || breadcrumb?.title,
|
||||
});
|
||||
}
|
||||
|
||||
return breadcrumbItems;
|
||||
};
|
||||
|
||||
// 处理搜索框输入
|
||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
};
|
||||
|
||||
// 处理搜索操作,仅当点击搜索按钮或按回车时执行
|
||||
const handleSearch = (value: string) => {
|
||||
if (value.trim() || !value) { // 允许空搜索打开高级搜索
|
||||
setSearchDialogVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AntHeader style={{
|
||||
padding: isMobile ? '0 10px' : '0 40px',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
height: isMobile ? 56 : 64,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
width: '100%',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
|
||||
onClick={toggleCollapsed}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
{!isMobile && (
|
||||
<Breadcrumb
|
||||
items={generateBreadcrumbItems()}
|
||||
style={{ marginLeft: 16 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 25 }}>
|
||||
{/* 搜索框 - 修复交互问题 */}
|
||||
{!isMobile && (
|
||||
<Search
|
||||
placeholder="搜索图片..."
|
||||
allowClear
|
||||
value={searchText}
|
||||
onChange={handleSearchInputChange}
|
||||
onSearch={handleSearch}
|
||||
style={{
|
||||
width: 300,
|
||||
borderRadius: 100
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<UserAvatar
|
||||
size={46}
|
||||
email={user?.email}
|
||||
text={user?.userName}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</AntHeader>
|
||||
|
||||
{/* 搜索对话框 - 传递搜索文本 */}
|
||||
<SearchDialog
|
||||
visible={searchDialogVisible}
|
||||
initialSearchText={searchText}
|
||||
onClose={() => {
|
||||
setSearchDialogVisible(false);
|
||||
// 可选:关闭对话框后清空搜索框
|
||||
// setSearchText('');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
173
Web/src/layouts/components/Sidebar.tsx
Normal file
173
Web/src/layouts/components/Sidebar.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from 'react';
|
||||
import { Layout, Menu, type MenuProps } from 'antd';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import routes from '../../config/routeConfig';
|
||||
import logo from '/logo.png';
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean;
|
||||
isMobile?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// 定义菜单项类型
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ collapsed, isMobile = false, onClose }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 菜单项样式
|
||||
const menuItemStyle = { fontSize: 15 };
|
||||
const iconStyle = { fontSize: 18 };
|
||||
|
||||
// 分组标题样式
|
||||
const groupTitleStyle = {
|
||||
fontSize: 12,
|
||||
color: '#8c8c8c',
|
||||
fontWeight: 500,
|
||||
marginLeft: collapsed ? 0 : 16,
|
||||
padding: collapsed ? '8px 0' : '8px 0'
|
||||
};
|
||||
|
||||
// 从路由配置生成菜单项
|
||||
const generateMenuItems = (): MenuItem[] => {
|
||||
const items: MenuItem[] = [];
|
||||
let lastGroup = '';
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.hideInMenu) return;
|
||||
|
||||
// 如果有分组标签且与上一个不同,添加分组
|
||||
if (route.groupLabel && route.groupLabel !== lastGroup && !collapsed) {
|
||||
items.push({
|
||||
type: 'group',
|
||||
label: <div style={groupTitleStyle}>{route.groupLabel}</div>,
|
||||
children: []
|
||||
} as MenuItem);
|
||||
lastGroup = route.groupLabel;
|
||||
}
|
||||
|
||||
// 如果需要添加分隔符
|
||||
if (route.divider) {
|
||||
items.push({
|
||||
type: 'divider'
|
||||
});
|
||||
}
|
||||
|
||||
// 添加菜单项
|
||||
items.push({
|
||||
key: route.path,
|
||||
icon: route.icon && React.isValidElement(route.icon)
|
||||
? React.cloneElement(route.icon as React.ReactElement<any>, { style: iconStyle })
|
||||
: route.icon,
|
||||
label: <span style={menuItemStyle}>{route.label}</span>
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
// 获取当前选中的菜单项
|
||||
const getSelectedKey = () => {
|
||||
const pathname = location.pathname;
|
||||
const matchedRoute = routes.find(route => {
|
||||
if (route.path.includes(':')) {
|
||||
const basePath = route.path.split(':')[0].replace(/\/$/, '');
|
||||
return pathname.startsWith('/' + basePath);
|
||||
}
|
||||
if (route.path === '/' && pathname === '/') {
|
||||
return true;
|
||||
}
|
||||
return pathname === '/' + route.path;
|
||||
});
|
||||
return matchedRoute ? (matchedRoute.path === '/' ? '/' : matchedRoute.path) : '/';
|
||||
};
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 遮罩层 - 仅在手机模式且侧边栏展开时显示 */}
|
||||
{isMobile && !collapsed && (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||
zIndex: 999, // 确保在Sider(1000)之下
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
width={isMobile ? 180 : 250}
|
||||
collapsedWidth={isMobile ? 0 : 80}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: isMobile ? 'absolute' : 'relative',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: isMobile ? 1000 : 1,
|
||||
boxShadow: isMobile && !collapsed ? '0 0 10px rgba(0,0,0,0.2)' : 'none',
|
||||
backgroundColor: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Logo区域 */}
|
||||
<div style={{
|
||||
height: isMobile ? '56px' : '64px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
padding: collapsed ? '0' : '0 20px',
|
||||
color: '#001529',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '18px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'white',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Foxel Logo"
|
||||
style={{
|
||||
height: collapsed ? '30px' : '32px',
|
||||
marginRight: collapsed ? '0' : '12px',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
/>
|
||||
{!collapsed && <span>Foxel</span>}
|
||||
</div>
|
||||
|
||||
{/* 侧边栏菜单 */}
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
defaultSelectedKeys={[getSelectedKey()]}
|
||||
items={generateMenuItems()}
|
||||
onClick={handleMenuClick}
|
||||
style={{
|
||||
borderRight: 'none',
|
||||
flex: 1
|
||||
}}
|
||||
/>
|
||||
</Sider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
Reference in New Issue
Block a user