添加布局组件

- 创建主布局框架(Header、Footer、MainLayout)
- 实现导航菜单和用户入口
- 添加响应式设计支持
This commit is contained in:
Leo 2025-10-09 23:45:07 +08:00
parent f7a1c8b580
commit b4947b89e1
6 changed files with 779 additions and 0 deletions

206
src/layout/Footer.css Normal file
View File

@ -0,0 +1,206 @@
/* 非遗文化传承网站 - Footer 样式 */
.heritage-footer {
background-color: #2c2c2c;
color: #ffffff;
margin-top: auto;
}
.footer-main {
padding: 60px 0 40px;
}
.footer-container {
max-width: 1440px;
margin: 0 auto;
padding: 0 50px;
}
/* Footer Section */
.footer-section {
height: 100%;
}
.footer-title {
color: #ffffff !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-bottom: 20px !important;
font-family: 'Noto Serif SC', 'Songti SC', serif;
}
.footer-description {
color: #cccccc !important;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px !important;
}
/* 社交媒体链接 */
.footer-social {
display: flex;
gap: 12px;
}
.social-link {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 50%;
color: #ffffff;
font-size: 18px;
transition: all 0.3s ease;
}
.social-link:hover {
background-color: #c8363d;
color: #ffffff;
transform: translateY(-2px);
}
/* Footer 链接 */
.footer-links {
list-style: none;
padding: 0;
margin: 0;
}
.footer-links li {
margin-bottom: 12px;
}
.footer-links a {
color: #cccccc;
font-size: 14px;
text-decoration: none;
transition: color 0.3s ease;
display: inline-block;
}
.footer-links a:hover {
color: #d4a574;
transform: translateX(4px);
}
/* 联系信息 */
.footer-contact {
width: 100%;
}
.contact-item {
display: flex;
align-items: flex-start;
gap: 8px;
color: #cccccc;
font-size: 14px;
}
.contact-icon {
color: #d4a574;
font-size: 16px;
margin-top: 2px;
}
.contact-item .ant-typography {
color: #cccccc;
font-size: 14px;
line-height: 1.6;
}
/* 分割线 */
.footer-divider {
border-top-color: rgba(255, 255, 255, 0.1);
margin: 0;
}
/* 底部版权区 */
.footer-bottom {
padding: 24px 0;
background-color: rgba(0, 0, 0, 0.2);
}
.footer-copyright {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.copyright-text {
color: #999999 !important;
font-size: 13px;
}
.footer-legal {
display: flex;
gap: 16px;
}
.footer-legal a {
color: #999999;
font-size: 13px;
text-decoration: none;
transition: color 0.3s ease;
}
.footer-legal a:hover {
color: #d4a574;
}
.footer-legal .ant-divider-vertical {
border-left-color: rgba(255, 255, 255, 0.2);
}
/* 响应式设计 */
@media (max-width: 992px) {
.footer-container {
padding: 0 24px;
}
.footer-main {
padding: 48px 0 32px;
}
.footer-copyright {
flex-direction: column;
align-items: flex-start;
text-align: left;
}
}
@media (max-width: 576px) {
.footer-container {
padding: 0 16px;
}
.footer-main {
padding: 40px 0 24px;
}
.footer-title {
font-size: 15px !important;
margin-bottom: 16px !important;
}
.footer-description {
font-size: 12px;
}
.footer-links a,
.contact-item .ant-typography {
font-size: 13px;
}
.footer-bottom {
padding: 20px 0;
}
.copyright-text,
.footer-legal a {
font-size: 12px;
}
}

154
src/layout/Footer.tsx Normal file
View File

@ -0,0 +1,154 @@
/**
* -
*/
import React from 'react'
import { Link } from 'react-router-dom'
import { Row, Col, Space, Typography, Divider } from 'antd'
import {
WechatOutlined,
WeiboOutlined,
MailOutlined,
PhoneOutlined,
EnvironmentOutlined,
} from '@ant-design/icons'
import './Footer.css'
const { Title, Text, Paragraph } = Typography
const Footer: React.FC = () => {
const currentYear = new Date().getFullYear()
return (
<footer className="heritage-footer">
<div className="footer-main">
<div className="footer-container">
<Row gutter={[48, 32]}>
{/* 关于我们 */}
<Col xs={24} sm={12} lg={6}>
<div className="footer-section">
<Title level={5} className="footer-title">
</Title>
<Paragraph className="footer-description">
广
</Paragraph>
<Space size="middle" className="footer-social">
<a href="#" className="social-link">
<WechatOutlined />
</a>
<a href="#" className="social-link">
<WeiboOutlined />
</a>
<a href="#" className="social-link">
<MailOutlined />
</a>
</Space>
</div>
</Col>
{/* 快速链接 */}
<Col xs={12} sm={12} lg={6}>
<div className="footer-section">
<Title level={5} className="footer-title">
</Title>
<ul className="footer-links">
<li>
<Link to="/heritage"></Link>
</li>
<li>
<Link to="/inheritors"></Link>
</li>
<li>
<Link to="/news"></Link>
</li>
<li>
<Link to="/data"></Link>
</li>
<li>
<Link to="/about"></Link>
</li>
</ul>
</div>
</Col>
{/* 项目分类 */}
<Col xs={12} sm={12} lg={6}>
<div className="footer-section">
<Title level={5} className="footer-title">
</Title>
<ul className="footer-links">
<li>
<Link to="/heritage/categories/traditional-craft"></Link>
</li>
<li>
<Link to="/heritage/categories/traditional-art"></Link>
</li>
<li>
<Link to="/heritage/categories/traditional-music"></Link>
</li>
<li>
<Link to="/heritage/categories/traditional-opera"></Link>
</li>
<li>
<Link to="/heritage/categories/folk-custom"></Link>
</li>
<li>
<Link to="/heritage/categories/traditional-medicine"></Link>
</li>
</ul>
</div>
</Col>
{/* 联系我们 */}
<Col xs={24} sm={12} lg={6}>
<div className="footer-section">
<Title level={5} className="footer-title">
</Title>
<Space direction="vertical" size="middle" className="footer-contact">
<div className="contact-item">
<PhoneOutlined className="contact-icon" />
<Text>400-123-4567</Text>
</div>
<div className="contact-item">
<MailOutlined className="contact-icon" />
<Text>heritage@example.com</Text>
</div>
<div className="contact-item">
<EnvironmentOutlined className="contact-icon" />
<Text></Text>
</div>
</Space>
</div>
</Col>
</Row>
</div>
</div>
{/* 版权信息 */}
<Divider className="footer-divider" />
<div className="footer-bottom">
<div className="footer-container">
<div className="footer-copyright">
<Text className="copyright-text">
© {currentYear} . All Rights Reserved.
</Text>
<Space split={<Divider type="vertical" />} className="footer-legal">
<Link to="/privacy"></Link>
<Link to="/terms"></Link>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener noreferrer">
ICP备xxxxxxxx号
</a>
</Space>
</div>
</div>
</div>
</footer>
)
}
export default Footer

169
src/layout/Header.css Normal file
View File

@ -0,0 +1,169 @@
/* 非遗文化传承网站 - Header 样式 */
.heritage-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid transparent;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
height: 64px;
}
.heritage-header.scrolled {
background-color: rgba(255, 255, 255, 0.98);
border-bottom-color: #e8e3db;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header-container {
max-width: 1440px;
margin: 0 auto;
padding: 0 50px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Logo 样式 */
.header-logo {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
transition: opacity 0.3s ease;
}
.header-logo:hover {
opacity: 0.8;
}
.logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #c8363d 0%, #8b252b 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 16px;
font-weight: 600;
font-family: 'Noto Serif SC', 'Songti SC', serif;
box-shadow: 0 2px 8px rgba(200, 54, 61, 0.3);
}
.logo-text {
display: flex;
flex-direction: column;
gap: 0;
}
.logo-title {
font-size: 18px;
font-weight: 600;
color: #2c2c2c;
line-height: 1.2;
font-family: 'Noto Serif SC', 'Songti SC', serif;
}
.logo-subtitle {
font-size: 11px;
color: #999999;
letter-spacing: 1px;
text-transform: uppercase;
line-height: 1;
}
/* 导航菜单样式 */
.header-nav {
flex: 1;
display: flex;
justify-content: center;
}
.header-menu {
border-bottom: none;
background: transparent;
min-width: 600px;
justify-content: center;
}
.header-menu .ant-menu-item,
.header-menu .ant-menu-submenu {
font-size: 14px;
font-weight: 500;
margin: 0 4px;
padding: 0 16px;
}
.header-menu .ant-menu-item a,
.header-menu .ant-menu-submenu-title {
color: #666666;
transition: color 0.3s ease;
}
.header-menu .ant-menu-item:hover a,
.header-menu .ant-menu-submenu:hover .ant-menu-submenu-title {
color: #c8363d;
}
.header-menu .ant-menu-item-selected a {
color: #c8363d;
}
/* 移动端菜单按钮 */
.mobile-menu-btn {
display: none;
font-size: 20px;
color: #2c2c2c;
}
/* 移动端抽屉样式 */
.mobile-menu-drawer .ant-drawer-body {
padding: 0;
}
.mobile-menu-drawer .ant-menu {
border-right: none;
}
/* 响应式设计 */
@media (max-width: 992px) {
.header-container {
padding: 0 24px;
}
.desktop-nav {
display: none;
}
.mobile-menu-btn {
display: flex;
}
}
@media (max-width: 576px) {
.header-container {
padding: 0 16px;
}
.logo-icon {
width: 36px;
height: 36px;
font-size: 14px;
}
.logo-title {
font-size: 16px;
}
.logo-subtitle {
font-size: 10px;
}
}

140
src/layout/Header.tsx Normal file
View File

@ -0,0 +1,140 @@
/**
* -
*/
import React, { useState, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Menu, Drawer, Button, Space } from 'antd'
import {
MenuOutlined,
HomeOutlined,
AppstoreOutlined,
TeamOutlined,
ExperimentOutlined,
FileTextOutlined,
InfoCircleOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import './Header.css'
type MenuItem = Required<MenuProps>['items'][number]
const Header: React.FC = () => {
const location = useLocation()
const [scrolled, setScrolled] = useState(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
// 监听滚动事件,添加导航栏背景效果
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// 导航菜单项
const menuItems: MenuItem[] = [
{
key: '/',
icon: <HomeOutlined />,
label: <Link to="/"></Link>,
},
{
key: '/heritage',
icon: <AppstoreOutlined />,
label: <Link to="/heritage"></Link>,
},
{
key: '/inheritors',
icon: <TeamOutlined />,
label: <Link to="/inheritors"></Link>,
},
{
key: '/news',
icon: <FileTextOutlined />,
label: <Link to="/news"></Link>,
},
{
key: '/about',
icon: <InfoCircleOutlined />,
label: <Link to="/about"></Link>,
},
]
// 获取当前选中的菜单项
const getSelectedKeys = () => {
const path = location.pathname
// 精确匹配或父级匹配
if (path === '/') return ['/']
if (path.startsWith('/heritage')) {
const categoryMatch = path.match(/\/heritage\/categories\/(.+)/)
if (categoryMatch) {
return [`/heritage/${categoryMatch[1]}`]
}
return ['/heritage']
}
if (path.startsWith('/inheritors')) return ['/inheritors']
if (path.startsWith('/learn')) return ['/learn']
if (path.startsWith('/news')) return ['/news']
if (path.startsWith('/about')) return ['/about']
if (path.startsWith('/data')) return ['/data']
if (path.startsWith('/search')) return ['/search']
if (path.startsWith('/user')) return ['/user']
return []
}
return (
<>
<header className={`heritage-header ${scrolled ? 'scrolled' : ''}`}>
<div className="header-container">
{/* Logo */}
<Link to="/" className="header-logo">
<div className="logo-icon"></div>
<div className="logo-text">
<div className="logo-title"></div>
<div className="logo-subtitle">Heritage</div>
</div>
</Link>
{/* 桌面导航菜单 */}
<nav className="header-nav desktop-nav">
<Menu
mode="horizontal"
selectedKeys={getSelectedKeys()}
items={menuItems}
className="header-menu"
/>
</nav>
{/* 移动端菜单按钮 */}
<Button
type="text"
icon={<MenuOutlined />}
className="mobile-menu-btn"
onClick={() => setMobileMenuOpen(true)}
/>
</div>
</header>
{/* 移动端抽屉菜单 */}
<Drawer
title="导航菜单"
placement="right"
onClose={() => setMobileMenuOpen(false)}
open={mobileMenuOpen}
className="mobile-menu-drawer"
>
<Menu
mode="inline"
selectedKeys={getSelectedKeys()}
items={menuItems}
onClick={() => setMobileMenuOpen(false)}
/>
</Drawer>
</>
)
}
export default Header

71
src/layout/MainLayout.css Normal file
View File

@ -0,0 +1,71 @@
/* 非遗文化传承网站 - MainLayout 样式 */
.main-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #fafaf8;
}
.main-content {
flex: 1;
margin-top: 64px; /* Header 高度 */
min-height: calc(100vh - 64px);
}
/* 返回顶部按钮样式 */
.back-to-top {
right: 40px;
bottom: 40px;
}
.back-to-top-button {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #c8363d 0%, #8b252b 100%);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
box-shadow: 0 4px 16px rgba(200, 54, 61, 0.3);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
cursor: pointer;
}
.back-to-top-button:hover {
transform: scale(1.1) translateY(-2px);
box-shadow: 0 6px 24px rgba(200, 54, 61, 0.4);
}
.back-to-top-button:active {
transform: scale(1.05);
}
/* 响应式设计 */
@media (max-width: 768px) {
.back-to-top {
right: 24px;
bottom: 24px;
}
.back-to-top-button {
width: 42px;
height: 42px;
font-size: 16px;
}
}
@media (max-width: 576px) {
.back-to-top {
right: 16px;
bottom: 16px;
}
.back-to-top-button {
width: 40px;
height: 40px;
font-size: 14px;
}
}

39
src/layout/MainLayout.tsx Normal file
View File

@ -0,0 +1,39 @@
/**
* -
*/
import React from 'react'
import { Outlet } from 'react-router-dom'
import { Layout, BackTop } from 'antd'
import { UpOutlined } from '@ant-design/icons'
import Header from './Header'
import Footer from './Footer'
import './MainLayout.css'
const { Content } = Layout
const MainLayout: React.FC = () => {
return (
<Layout className="main-layout">
{/* 头部导航 */}
<Header />
{/* 主内容区 */}
<Content className="main-content">
<Outlet />
</Content>
{/* 页脚 */}
<Footer />
{/* 返回顶部按钮 */}
<BackTop className="back-to-top">
<div className="back-to-top-button">
<UpOutlined />
</div>
</BackTop>
</Layout>
)
}
export default MainLayout