添加核心页面
- 首页:轮播图、特色项目、传承人展示、最新资讯等 - 非遗项目页面:列表页和详情页,支持筛选和排序 - 传承人页面:列表页和详情页,展示个人作品和技艺 - 关于页面:核心价值观、使命愿景展示 - 搜索页面:全站搜索功能 - 数据可视化页面:统计图表展示 - 用户中心:登录、注册、个人信息管理
This commit is contained in:
parent
f46513ab8b
commit
6257ce5c7b
202
src/pages/About/index.css
Normal file
202
src/pages/About/index.css
Normal file
@ -0,0 +1,202 @@
|
||||
/* 关于我们页面样式 */
|
||||
|
||||
.about-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #4a5f7f 0%, #2a3f5f 100%);
|
||||
padding: 80px 0 60px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mission-section {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.values-section {
|
||||
background: linear-gradient(135deg, #fff9f0 0%, #f5f0e8 100%);
|
||||
}
|
||||
|
||||
.value-card {
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
padding: 40px 32px;
|
||||
background: #ffffff;
|
||||
border: 2px solid #f0f0f0;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-card:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: var(--gradient-end);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 定义各卡片的渐变色 */
|
||||
.value-card[data-color='red'] {
|
||||
--gradient-start: #ff6b6b;
|
||||
--gradient-end: #c8363d;
|
||||
--icon-bg: rgba(200, 54, 61, 0.1);
|
||||
--icon-color: #c8363d;
|
||||
}
|
||||
|
||||
.value-card[data-color='gold'] {
|
||||
--gradient-start: #ffd89b;
|
||||
--gradient-end: #d4a574;
|
||||
--icon-bg: rgba(212, 165, 116, 0.1);
|
||||
--icon-color: #d4a574;
|
||||
}
|
||||
|
||||
.value-card[data-color='green'] {
|
||||
--gradient-start: #52b788;
|
||||
--gradient-end: #2a5e4d;
|
||||
--icon-bg: rgba(42, 94, 77, 0.1);
|
||||
--icon-color: #2a5e4d;
|
||||
}
|
||||
|
||||
.value-card[data-color='blue'] {
|
||||
--gradient-start: #7b9fd4;
|
||||
--gradient-end: #4a5f7f;
|
||||
--icon-bg: rgba(74, 95, 127, 0.1);
|
||||
--icon-color: #4a5f7f;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.value-card-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 图标容器 */
|
||||
.value-icon-circle {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: 0 auto;
|
||||
background: var(--icon-bg);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.value-card:hover .value-icon-circle {
|
||||
transform: scale(1.1);
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
}
|
||||
|
||||
.value-icon-circle .anticon {
|
||||
font-size: 42px;
|
||||
color: var(--icon-color);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.value-card:hover .value-icon-circle .anticon {
|
||||
color: #ffffff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 文字区域 */
|
||||
.value-card-body {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.value-card-body h4 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 12px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
letter-spacing: 2px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.value-card:hover .value-card-body h4 {
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.value-card-body .ant-typography {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.contact-section .ant-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.contact-section .ant-typography {
|
||||
font-size: 15px;
|
||||
line-height: 2;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.value-card {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.value-icon-circle {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.value-icon-circle .anticon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.value-card-body h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.value-card-body .ant-typography {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
123
src/pages/About/index.tsx
Normal file
123
src/pages/About/index.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 关于我们页面
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Row, Col, Typography, Card } from 'antd'
|
||||
import { HeartOutlined, BulbOutlined, GlobalOutlined, StarOutlined } from '@ant-design/icons'
|
||||
import './index.css'
|
||||
|
||||
const { Title, Paragraph } = Typography
|
||||
|
||||
const About: React.FC = () => {
|
||||
return (
|
||||
<div className="about-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>关于我们</h1>
|
||||
<p>让千年技艺重焕新生,用数字科技守护文化根脉</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="about-content">
|
||||
<section className="mission-section section-spacing">
|
||||
<div className="container">
|
||||
<div className="text-center" style={{ maxWidth: 800, margin: '0 auto' }}>
|
||||
<Title level={2}>我们的使命</Title>
|
||||
<Paragraph style={{ fontSize: 16, lineHeight: 1.8 }}>
|
||||
非遗传承平台致力于中国非物质文化遗产的数字化保护、传承与推广。
|
||||
我们通过现代科技手段,让传统文化在当代社会焕发新的生命力,
|
||||
让更多人了解、学习和传承这些珍贵的文化瑰宝。
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="values-section section-spacing gradient-bg-warm">
|
||||
<div className="container">
|
||||
<div className="text-center" style={{ marginBottom: 48 }}>
|
||||
<Title level={2}>核心价值观</Title>
|
||||
</div>
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<div className="value-card" data-color="red">
|
||||
<div className="value-card-header">
|
||||
<div className="value-icon-circle">
|
||||
<HeartOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="value-card-body">
|
||||
<Title level={4}>传承</Title>
|
||||
<Paragraph>保护和传承中华民族优秀传统文化</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<div className="value-card" data-color="gold">
|
||||
<div className="value-card-header">
|
||||
<div className="value-icon-circle">
|
||||
<BulbOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="value-card-body">
|
||||
<Title level={4}>创新</Title>
|
||||
<Paragraph>用现代科技赋能传统文化传承</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<div className="value-card" data-color="green">
|
||||
<div className="value-card-header">
|
||||
<div className="value-icon-circle">
|
||||
<GlobalOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="value-card-body">
|
||||
<Title level={4}>开放</Title>
|
||||
<Paragraph>打造开放共享的文化传承平台</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<div className="value-card" data-color="blue">
|
||||
<div className="value-card-header">
|
||||
<div className="value-icon-circle">
|
||||
<StarOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="value-card-body">
|
||||
<Title level={4}>卓越</Title>
|
||||
<Paragraph>追求卓越品质,打造精品服务</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="contact-section section-spacing">
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<Title level={2}>联系我们</Title>
|
||||
<Paragraph style={{ fontSize: 16, marginBottom: 32 }}>
|
||||
如有任何问题或建议,欢迎随时与我们联系
|
||||
</Paragraph>
|
||||
<div style={{ maxWidth: 600, margin: '0 auto' }}>
|
||||
<Card>
|
||||
<Paragraph><strong>地址:</strong>北京市朝阳区文化创意产业园</Paragraph>
|
||||
<Paragraph><strong>电话:</strong>400-123-4567</Paragraph>
|
||||
<Paragraph><strong>邮箱:</strong>heritage@example.com</Paragraph>
|
||||
<Paragraph style={{ marginBottom: 0 }}>
|
||||
<strong>工作时间:</strong>周一至周五 9:00-18:00
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
414
src/pages/Data/index.css
Normal file
414
src/pages/Data/index.css
Normal file
@ -0,0 +1,414 @@
|
||||
/* 数据可视化页样式 */
|
||||
|
||||
.data-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #3d5a80 0%, #2a4a6a 100%);
|
||||
padding: 80px 0 60px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 总体统计 */
|
||||
.stats-overview {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stats-overview .ant-card {
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-overview .ant-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.stats-overview .ant-statistic-title {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stats-overview .ant-statistic-content {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 数据卡片 */
|
||||
.data-card {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.data-card .ant-card-head {
|
||||
border-bottom: 2px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.data-card .ant-card-head-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
/* 省份分布图 */
|
||||
.province-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.province-item {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 100px 1fr 60px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.province-rank {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #d4a574 0%, #c8a060 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.province-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.province-bar-wrapper {
|
||||
background: #f5f0e8;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.province-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #d4a574 0%, #c8a060 100%);
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
.province-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 类别分布图 */
|
||||
.category-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 级别分布图 */
|
||||
.level-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.level-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #fafaf8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.level-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.level-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.level-count {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.level-percentage {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
/* 年龄分布图 */
|
||||
.age-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.age-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.age-range {
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.age-bar-wrapper {
|
||||
flex: 1;
|
||||
background: #f5f0e8;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.age-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #c8363d 0%, #a82f35 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 12px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
.age-count {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 性别分布图 */
|
||||
.gender-chart {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.gender-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gender-item {
|
||||
text-align: center;
|
||||
padding: 32px 20px;
|
||||
background: #fafaf8;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.gender-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.gender-item.male {
|
||||
border: 2px solid #3d5a80;
|
||||
}
|
||||
|
||||
.gender-item.female {
|
||||
border: 2px solid #c8363d;
|
||||
}
|
||||
|
||||
.gender-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gender-label {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gender-count {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gender-percentage {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
/* 课程排行 */
|
||||
.course-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
background: #fafaf8;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.course-item:hover {
|
||||
background: #f5f0e8;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.course-rank {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #c8363d 0%, #a82f35 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.course-instructor::before {
|
||||
content: '👨🏫 ';
|
||||
}
|
||||
|
||||
.course-enroll {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.enroll-count {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #d4a574;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.enroll-label {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.province-item {
|
||||
grid-template-columns: 32px 80px 1fr 50px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gender-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.course-enroll {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.stats-overview .ant-statistic-content {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.province-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.province-bar-wrapper {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.age-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.age-range {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
254
src/pages/Data/index.tsx
Normal file
254
src/pages/Data/index.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 数据可视化页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Row, Col, Card, Statistic, Progress, Tag } from 'antd'
|
||||
import {
|
||||
GlobalOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import * as mockData from '@services/mockData'
|
||||
import './index.css'
|
||||
|
||||
interface ProvinceStats {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
}
|
||||
|
||||
interface CategoryStats {
|
||||
name: string
|
||||
count: number
|
||||
color: string
|
||||
}
|
||||
|
||||
const DataVisualization: React.FC = () => {
|
||||
const [provinceStats, setProvinceStats] = useState<ProvinceStats[]>([])
|
||||
const [categoryStats, setCategoryStats] = useState<CategoryStats[]>([])
|
||||
const [levelStats, setLevelStats] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
calculateStats()
|
||||
}, [])
|
||||
|
||||
const calculateStats = () => {
|
||||
// 计算省份统计
|
||||
const provinceMap = new Map<string, number>()
|
||||
mockData.mockHeritageItems.forEach((item) => {
|
||||
provinceMap.set(item.province, (provinceMap.get(item.province) || 0) + 1)
|
||||
})
|
||||
|
||||
const totalItems = mockData.mockHeritageItems.length
|
||||
const provinces: ProvinceStats[] = Array.from(provinceMap.entries())
|
||||
.map(([name, count]) => ({
|
||||
name,
|
||||
count,
|
||||
percentage: Math.round((count / totalItems) * 100),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10)
|
||||
|
||||
setProvinceStats(provinces)
|
||||
|
||||
// 计算类别统计
|
||||
const categoryMap = new Map<string, number>()
|
||||
mockData.mockHeritageItems.forEach((item) => {
|
||||
categoryMap.set(item.category, (categoryMap.get(item.category) || 0) + 1)
|
||||
})
|
||||
|
||||
const colors = ['#c8363d', '#d4a574', '#2a5e40', '#3d5a80', '#8b4513']
|
||||
const categories: CategoryStats[] = Array.from(categoryMap.entries())
|
||||
.map(([name, count], index) => ({
|
||||
name,
|
||||
count,
|
||||
color: colors[index % colors.length],
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
|
||||
setCategoryStats(categories)
|
||||
|
||||
// 计算级别统计
|
||||
const levelMap = new Map<string, number>()
|
||||
mockData.mockHeritageItems.forEach((item) => {
|
||||
levelMap.set(item.level, (levelMap.get(item.level) || 0) + 1)
|
||||
})
|
||||
|
||||
const levelLabels: Record<string, string> = {
|
||||
world: '世界级',
|
||||
national: '国家级',
|
||||
provincial: '省级',
|
||||
municipal: '市级',
|
||||
county: '县级',
|
||||
}
|
||||
|
||||
const levels = Array.from(levelMap.entries())
|
||||
.map(([key, count]) => ({
|
||||
name: levelLabels[key] || key,
|
||||
count,
|
||||
percentage: Math.round((count / totalItems) * 100),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
|
||||
setLevelStats(levels)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="data-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>数据可视化</h1>
|
||||
<p>非遗文化传承数据统计与分析</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content section-spacing">
|
||||
<div className="container">
|
||||
{/* 总体统计 */}
|
||||
<Row gutter={[24, 24]} className="stats-overview">
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="非遗项目总数"
|
||||
value={mockData.mockHeritageItems.length}
|
||||
prefix={<GlobalOutlined />}
|
||||
valueStyle={{ color: '#c8363d' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="传承人总数"
|
||||
value={mockData.mockInheritors.length}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: '#d4a574' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 省份分布 */}
|
||||
<Card title="省份分布 TOP 10" className="data-card">
|
||||
<div className="province-chart">
|
||||
{provinceStats.map((item, index) => (
|
||||
<div key={item.name} className="province-item">
|
||||
<div className="province-rank">{index + 1}</div>
|
||||
<div className="province-name">{item.name}</div>
|
||||
<div className="province-bar-wrapper">
|
||||
<div
|
||||
className="province-bar"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="province-count">{item.count}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* 类别分布 */}
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="类别分布" className="data-card">
|
||||
<div className="category-chart">
|
||||
{categoryStats.map((item) => (
|
||||
<div key={item.name} className="category-item">
|
||||
<div className="category-info">
|
||||
<Tag color={item.color}>{item.name}</Tag>
|
||||
<span className="category-count">{item.count} 项</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((item.count / mockData.mockHeritageItems.length) * 100)}
|
||||
strokeColor={item.color}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 级别分布 */}
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="级别分布" className="data-card">
|
||||
<div className="level-chart">
|
||||
{levelStats.map((item, index) => (
|
||||
<div key={item.name} className="level-item">
|
||||
<div className="level-info">
|
||||
<span className="level-name">{item.name}</span>
|
||||
<span className="level-count">{item.count} 项</span>
|
||||
</div>
|
||||
<div className="level-percentage">{item.percentage}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 传承人统计 */}
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="传承人年龄分布" className="data-card">
|
||||
<div className="age-chart">
|
||||
{[
|
||||
{ range: '30岁以下', count: mockData.mockInheritors.filter(i => i.age < 30).length },
|
||||
{ range: '30-50岁', count: mockData.mockInheritors.filter(i => i.age >= 30 && i.age <= 50).length },
|
||||
{ range: '50-70岁', count: mockData.mockInheritors.filter(i => i.age > 50 && i.age <= 70).length },
|
||||
{ range: '70岁以上', count: mockData.mockInheritors.filter(i => i.age > 70).length },
|
||||
].map((item) => (
|
||||
<div key={item.range} className="age-item">
|
||||
<div className="age-range">{item.range}</div>
|
||||
<div className="age-bar-wrapper">
|
||||
<div
|
||||
className="age-bar"
|
||||
style={{
|
||||
width: `${(item.count / mockData.mockInheritors.length) * 100}%`,
|
||||
}}
|
||||
>
|
||||
<span className="age-count">{item.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="传承人性别分布" className="data-card">
|
||||
<div className="gender-chart">
|
||||
<div className="gender-stats">
|
||||
<div className="gender-item male">
|
||||
<div className="gender-icon">👨</div>
|
||||
<div className="gender-label">男性</div>
|
||||
<div className="gender-count">
|
||||
{mockData.mockInheritors.filter(i => i.gender === 'male').length}
|
||||
</div>
|
||||
<div className="gender-percentage">
|
||||
{Math.round((mockData.mockInheritors.filter(i => i.gender === 'male').length / mockData.mockInheritors.length) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="gender-item female">
|
||||
<div className="gender-icon">👩</div>
|
||||
<div className="gender-label">女性</div>
|
||||
<div className="gender-count">
|
||||
{mockData.mockInheritors.filter(i => i.gender === 'female').length}
|
||||
</div>
|
||||
<div className="gender-percentage">
|
||||
{Math.round((mockData.mockInheritors.filter(i => i.gender === 'female').length / mockData.mockInheritors.length) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataVisualization
|
||||
126
src/pages/Heritage/Detail.css
Normal file
126
src/pages/Heritage/Detail.css
Normal file
@ -0,0 +1,126 @@
|
||||
/* 非遗项目详情页样式 */
|
||||
|
||||
.heritage-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.page-breadcrumb {
|
||||
padding: 24px 0;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.detail-meta .anticon {
|
||||
margin-right: 6px;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
.detail-gallery {
|
||||
margin-bottom: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-gallery .ant-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.detail-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sidebar-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-info,
|
||||
.sidebar-tags {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sidebar-info h3,
|
||||
.sidebar-tags h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 80px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #2c2c2c;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.detail-sidebar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
178
src/pages/Heritage/Detail.tsx
Normal file
178
src/pages/Heritage/Detail.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 非遗项目详情页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Space } from 'antd'
|
||||
import { HomeOutlined, EnvironmentOutlined, EyeOutlined, HeartOutlined } from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { HeritageItem } from '@types/index'
|
||||
import { getHeritageById } from '@services/api'
|
||||
import FavoriteButton from '@components/FavoriteButton'
|
||||
import CommentSection from '@components/CommentSection'
|
||||
import './Detail.css'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
const levelLabels: Record<string, string> = {
|
||||
world: '世界级',
|
||||
national: '国家级',
|
||||
provincial: '省级',
|
||||
municipal: '市级',
|
||||
county: '县级',
|
||||
}
|
||||
|
||||
const HeritageDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<HeritageItem | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchData(id)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchData = async (heritageId: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getHeritageById(heritageId)
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch heritage detail:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="empty-container">
|
||||
<Empty description="未找到相关项目" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="heritage-detail-page">
|
||||
<div className="page-breadcrumb">
|
||||
<div className="container">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/"><HomeOutlined /> 首页</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/heritage">非遗项目</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{data.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-content section-spacing-sm">
|
||||
<div className="container">
|
||||
<Row gutter={[40, 40]}>
|
||||
<Col xs={24} lg={16}>
|
||||
{/* 头部信息 */}
|
||||
<div className="detail-header">
|
||||
<h1 className="detail-title">{data.name}</h1>
|
||||
<div className="detail-meta">
|
||||
<Tag color="red">{levelLabels[data.level]}</Tag>
|
||||
<span><EnvironmentOutlined /> {data.province}</span>
|
||||
<span><EyeOutlined /> {data.viewCount.toLocaleString()}</span>
|
||||
<span><HeartOutlined /> {data.likeCount.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图片画廊 */}
|
||||
<div className="detail-gallery">
|
||||
<Image.PreviewGroup>
|
||||
{data.images?.map((img, index) => (
|
||||
<Image key={index} src={img} />
|
||||
))}
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
|
||||
{/* 详细内容 */}
|
||||
<Tabs defaultActiveKey="description" className="detail-tabs">
|
||||
<TabPane tab="项目介绍" key="description">
|
||||
<div className="detail-text">
|
||||
<p>{data.description}</p>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="历史渊源" key="history">
|
||||
<div className="detail-text">
|
||||
<p>{data.history}</p>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="技艺特点" key="skills">
|
||||
<div className="detail-text">
|
||||
<p>{data.skills}</p>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="文化意义" key="significance">
|
||||
<div className="detail-text">
|
||||
<p>{data.significance}</p>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
{/* 评论区 */}
|
||||
<CommentSection targetType="heritage" targetId={data.id} showRating />
|
||||
</Col>
|
||||
|
||||
<Col xs={24} lg={8}>
|
||||
<div className="detail-sidebar">
|
||||
<div className="sidebar-actions">
|
||||
<FavoriteButton heritageId={data.id} size="large" />
|
||||
</div>
|
||||
|
||||
<div className="sidebar-info">
|
||||
<h3>基本信息</h3>
|
||||
<div className="info-item">
|
||||
<span className="info-label">分类:</span>
|
||||
<span className="info-value">{data.category}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">级别:</span>
|
||||
<span className="info-value">{levelLabels[data.level]}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">地区:</span>
|
||||
<span className="info-value">{data.province}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">状态:</span>
|
||||
<span className="info-value">{data.status === 'active' ? '正常传承' : '濒危'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.tags.length > 0 && (
|
||||
<div className="sidebar-tags">
|
||||
<h3>相关标签</h3>
|
||||
<Space wrap>
|
||||
{data.tags.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeritageDetail
|
||||
161
src/pages/Heritage/List.css
Normal file
161
src/pages/Heritage/List.css
Normal file
@ -0,0 +1,161 @@
|
||||
/* 非遗项目列表页样式 */
|
||||
|
||||
.heritage-list-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #c8363d 0%, #8b252b 100%);
|
||||
padding: 80px 0 60px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
/* 筛选器区域样式 */
|
||||
.filter-section {
|
||||
background: #f5f0e8;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-controls .ant-input-affix-wrapper {
|
||||
height: 40px !important;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
padding: 4px 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-controls .ant-input-affix-wrapper .ant-input {
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-controls .ant-input-affix-wrapper .ant-input-prefix {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filter-controls .ant-select {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-controls .ant-select-selector {
|
||||
height: 40px !important;
|
||||
border-radius: 8px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-controls .ant-btn {
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 筛选结果提示 */
|
||||
.filter-result-info {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
border-left: 3px solid #c8363d;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
display: inline-block;
|
||||
background: #fff9f0;
|
||||
color: #c8363d;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
border: 1px solid rgba(200, 54, 61, 0.2);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-controls .ant-input,
|
||||
.filter-controls .ant-select,
|
||||
.filter-controls .ant-btn {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.filter-result-info {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.filter-label,
|
||||
.filter-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
287
src/pages/Heritage/List.tsx
Normal file
287
src/pages/Heritage/List.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 非遗项目列表页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Row, Col, Spin, Empty, Input, Select, Space, Button } from 'antd'
|
||||
import { SearchOutlined, FilterOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import HeritageCard from '@components/HeritageCard'
|
||||
import CustomPagination from '@components/CustomPagination'
|
||||
import { getHeritageList } from '@services/api'
|
||||
import type { HeritageItem, PaginationResult, HeritageCategory } from '@types/index'
|
||||
import './List.css'
|
||||
|
||||
// 分类选项
|
||||
const categoryOptions = [
|
||||
{ label: '全部分类', value: '' },
|
||||
{ label: '民间文学', value: 'folk-literature' },
|
||||
{ label: '传统音乐', value: 'traditional-music' },
|
||||
{ label: '传统舞蹈', value: 'traditional-dance' },
|
||||
{ label: '传统戏剧', value: 'traditional-opera' },
|
||||
{ label: '曲艺', value: 'folk-art' },
|
||||
{ label: '传统体育、游艺与杂技', value: 'sports-acrobatics' },
|
||||
{ label: '传统技艺', value: 'traditional-craft' },
|
||||
{ label: '传统医药', value: 'traditional-medicine' },
|
||||
{ label: '民俗', value: 'folk-custom' },
|
||||
{ label: '传统美术', value: 'traditional-art' },
|
||||
]
|
||||
|
||||
// 常用标签选项
|
||||
const tagOptions = [
|
||||
'传统技艺',
|
||||
'传统美术',
|
||||
'传统音乐',
|
||||
'传统戏剧',
|
||||
'民俗',
|
||||
'金属工艺',
|
||||
'刺绣',
|
||||
'陶瓷',
|
||||
'古琴',
|
||||
'书法',
|
||||
'造纸',
|
||||
'宫廷艺术',
|
||||
'江南文化',
|
||||
'文人艺术',
|
||||
'文房四宝',
|
||||
]
|
||||
|
||||
const HeritageList: React.FC = () => {
|
||||
const [data, setData] = useState<PaginationResult<HeritageItem> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(12)
|
||||
|
||||
// 筛选条件状态
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||
|
||||
// 实际应用的筛选条件(用于提交后生效)
|
||||
const [appliedFilters, setAppliedFilters] = useState({
|
||||
keyword: '',
|
||||
category: '',
|
||||
tags: [] as string[],
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 获取所有数据(设置一个很大的 pageSize)
|
||||
const result = await getHeritageList({ page: 1, pageSize: 1000 })
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch heritage list:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
// 只改变页码,不改变 pageSize
|
||||
if (size === pageSize) {
|
||||
setCurrentPage(page)
|
||||
} else {
|
||||
// pageSize 改变时,重置到第一页
|
||||
setCurrentPage(1)
|
||||
setPageSize(size)
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleShowSizeChange = (current: number, size: number) => {
|
||||
setCurrentPage(1)
|
||||
setPageSize(size)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
const handleApplyFilters = () => {
|
||||
setAppliedFilters({
|
||||
keyword: searchKeyword,
|
||||
category: selectedCategory,
|
||||
tags: selectedTags,
|
||||
})
|
||||
setCurrentPage(1)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
const handleResetFilters = () => {
|
||||
setSearchKeyword('')
|
||||
setSelectedCategory('')
|
||||
setSelectedTags([])
|
||||
setAppliedFilters({
|
||||
keyword: '',
|
||||
category: '',
|
||||
tags: [],
|
||||
})
|
||||
setCurrentPage(1)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
// 客户端筛选和分页数据
|
||||
const getFilteredData = () => {
|
||||
if (!data) return null
|
||||
|
||||
let filteredItems = [...data.data]
|
||||
|
||||
// 按名字搜索
|
||||
if (appliedFilters.keyword) {
|
||||
const keyword = appliedFilters.keyword.toLowerCase()
|
||||
filteredItems = filteredItems.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.description.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 按分类筛选
|
||||
if (appliedFilters.category) {
|
||||
filteredItems = filteredItems.filter((item) => item.category === appliedFilters.category)
|
||||
}
|
||||
|
||||
// 按标签筛选
|
||||
if (appliedFilters.tags.length > 0) {
|
||||
filteredItems = filteredItems.filter((item) =>
|
||||
appliedFilters.tags.some((tag) => item.tags.includes(tag))
|
||||
)
|
||||
}
|
||||
|
||||
// 客户端分页
|
||||
const total = filteredItems.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (currentPage - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const paginatedItems = filteredItems.slice(start, end)
|
||||
|
||||
return {
|
||||
data: paginatedItems,
|
||||
total: total,
|
||||
page: currentPage,
|
||||
pageSize: pageSize,
|
||||
totalPages: totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
const filteredData = getFilteredData()
|
||||
|
||||
return (
|
||||
<div className="heritage-list-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>非遗项目</h1>
|
||||
<p>探索中华民族丰富多彩的非物质文化遗产</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content section-spacing">
|
||||
<div className="container-wide">
|
||||
{/* 筛选器区域 */}
|
||||
<div className="filter-section">
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Space wrap size="middle" className="filter-controls">
|
||||
{/* 搜索框 */}
|
||||
<Input
|
||||
placeholder="搜索非遗项目名称或描述"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onPressEnter={handleApplyFilters}
|
||||
style={{ width: 280 }}
|
||||
allowClear
|
||||
/>
|
||||
|
||||
{/* 分类选择 */}
|
||||
<Select
|
||||
placeholder="选择分类"
|
||||
value={selectedCategory || undefined}
|
||||
onChange={(value) => setSelectedCategory(value || '')}
|
||||
style={{ width: 200 }}
|
||||
options={categoryOptions}
|
||||
allowClear
|
||||
/>
|
||||
|
||||
{/* 标签选择 */}
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择标签"
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
style={{ minWidth: 200, maxWidth: 400 }}
|
||||
options={tagOptions.map((tag) => ({ label: tag, value: tag }))}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
/>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Button type="primary" icon={<FilterOutlined />} onClick={handleApplyFilters}>
|
||||
筛选
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleResetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* 筛选结果提示 */}
|
||||
{(appliedFilters.keyword || appliedFilters.category || appliedFilters.tags.length > 0) && (
|
||||
<div className="filter-result-info">
|
||||
<Space wrap size="small">
|
||||
<span className="filter-label">当前筛选:</span>
|
||||
{appliedFilters.keyword && (
|
||||
<span className="filter-tag">关键词: {appliedFilters.keyword}</span>
|
||||
)}
|
||||
{appliedFilters.category && (
|
||||
<span className="filter-tag">
|
||||
分类:{' '}
|
||||
{categoryOptions.find((opt) => opt.value === appliedFilters.category)
|
||||
?.label}
|
||||
</span>
|
||||
)}
|
||||
{appliedFilters.tags.map((tag) => (
|
||||
<span key={tag} className="filter-tag">
|
||||
标签: {tag}
|
||||
</span>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : filteredData && filteredData.data.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{filteredData.data.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<HeritageCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<CustomPagination
|
||||
current={currentPage}
|
||||
total={filteredData.total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onShowSizeChange={handleShowSizeChange}
|
||||
unit="项"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无数据" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeritageList
|
||||
558
src/pages/Home/index.css
Normal file
558
src/pages/Home/index.css
Normal file
@ -0,0 +1,558 @@
|
||||
/* 首页样式 */
|
||||
|
||||
.home-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== Hero Section ===== */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-carousel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hero-carousel .slick-slide {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
animation: ken-burns 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes ken-burns {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(200, 54, 61, 0.7) 0%,
|
||||
rgba(139, 37, 43, 0.8) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 40px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn {
|
||||
height: 48px;
|
||||
padding: 0 40px;
|
||||
font-size: 16px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn-primary {
|
||||
background: #ffffff;
|
||||
color: #c8363d;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn-primary:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn:not(.ant-btn-primary) {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn:not(.ant-btn-primary):hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: #ffffff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== Values Section ===== */
|
||||
.values-section {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fafaf8 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.value-card {
|
||||
text-align: center;
|
||||
padding: 48px 32px;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #f0ebe3;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.value-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--icon-color, #C8363D) 0%, transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.value-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.value-card:hover {
|
||||
transform: translateY(-12px);
|
||||
border-color: var(--icon-color, #C8363D);
|
||||
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.value-icon-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.value-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--icon-color, #C8363D), rgba(var(--icon-color-rgb, 200, 54, 61), 0.7));
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
transform: rotate(45deg);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.value-card:hover .value-icon {
|
||||
transform: rotate(45deg) scale(1.1);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
transform: rotate(-45deg);
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.value-decoration {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid var(--icon-color, #C8363D);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.value-card:hover .value-decoration {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.value-card h3 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.value-divider {
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--icon-color, #C8363D) 0%, transparent 100%);
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.value-card p {
|
||||
font-size: 15px;
|
||||
color: #666666;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== Categories Section ===== */
|
||||
.categories-section {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
display: block;
|
||||
padding: 32px 24px;
|
||||
background: var(--category-bg, #ffffff);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
min-height: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.category-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--category-color, #C8363D);
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.category-card:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
||||
border-color: var(--category-color, #C8363D);
|
||||
}
|
||||
|
||||
.category-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
letter-spacing: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-card:hover .category-title {
|
||||
color: var(--category-color, #C8363D);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-card:hover .category-desc {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.category-corner {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-top: 3px solid var(--category-color, #C8363D);
|
||||
border-right: 3px solid var(--category-color, #C8363D);
|
||||
opacity: 0;
|
||||
transform: translate(10px, -10px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.category-card:hover .category-corner {
|
||||
opacity: 0.3;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.category-pattern {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
right: -20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle, var(--category-color, #C8363D) 0%, transparent 70%);
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.category-card:hover .category-pattern {
|
||||
opacity: 0.1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* ===== Section Header ===== */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.section-header.text-center {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin: 0 !important;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin: 8px 0 0 0 !important;
|
||||
}
|
||||
|
||||
.section-header a {
|
||||
color: #c8363d;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section-header a:hover {
|
||||
color: #a82e34;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* ===== Featured Section ===== */
|
||||
.featured-section {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ===== Inheritors Section ===== */
|
||||
.inheritors-section {
|
||||
background: linear-gradient(135deg, #fff9f0 0%, #f5f0e8 100%);
|
||||
}
|
||||
|
||||
/* ===== Statistics Section ===== */
|
||||
.statistics-section {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 32px 24px;
|
||||
background: #f5f0e8;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card .ant-statistic {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-card .ant-statistic-title {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .ant-statistic-content {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #c8363d;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.stat-card .anticon {
|
||||
font-size: 24px;
|
||||
color: #d4a574;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== CTA Section ===== */
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, #c8363d 0%, #8b252b 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cta-section .ant-btn-default.ant-btn-background-ghost {
|
||||
border-color: #ffffff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cta-section .ant-btn-default.ant-btn-background-ghost:hover {
|
||||
background: #ffffff !important;
|
||||
border-color: #ffffff !important;
|
||||
color: #c8363d !important;
|
||||
}
|
||||
|
||||
.cta-section .ant-btn:not(.ant-btn-background-ghost):hover {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 992px) {
|
||||
.hero-section,
|
||||
.hero-carousel,
|
||||
.hero-carousel .slick-slide,
|
||||
.hero-slide {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section,
|
||||
.hero-carousel,
|
||||
.hero-carousel .slick-slide,
|
||||
.hero-slide {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hero-actions .ant-btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.value-card {
|
||||
padding: 36px 24px;
|
||||
}
|
||||
|
||||
.value-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.value-card h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-card .ant-statistic-content {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
padding: 24px 20px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
312
src/pages/Home/index.tsx
Normal file
312
src/pages/Home/index.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
/**
|
||||
* 首页组件
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Row, Col, Typography, Button, Statistic, Space, Carousel } from 'antd'
|
||||
import {
|
||||
ArrowRightOutlined,
|
||||
GlobalOutlined,
|
||||
TrophyOutlined,
|
||||
TeamOutlined,
|
||||
EnvironmentOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import HeritageCard from '@components/HeritageCard'
|
||||
import InheritorCard from '@components/InheritorCard'
|
||||
import {
|
||||
getStatistics,
|
||||
getFeaturedHeritage,
|
||||
getFeaturedInheritors,
|
||||
} from '@services/api'
|
||||
import type { HeritageItem, Inheritor, Statistics } from '@types/index'
|
||||
import './index.css'
|
||||
|
||||
const { Title, Paragraph } = Typography
|
||||
|
||||
const categories = [
|
||||
{ key: 'traditional-craft', label: '传统技艺', desc: '千年匠心', color: '#C8363D', bgGradient: 'linear-gradient(135deg, #FFEAE7 0%, #FFD6D1 100%)' },
|
||||
{ key: 'traditional-art', label: '传统美术', desc: '丹青妙笔', color: '#D4A574', bgGradient: 'linear-gradient(135deg, #FFF4E6 0%, #FFE7C8 100%)' },
|
||||
{ key: 'traditional-music', label: '传统音乐', desc: '余音绕梁', color: '#2A5E4D', bgGradient: 'linear-gradient(135deg, #E6F4F1 0%, #CCE8E1 100%)' },
|
||||
{ key: 'traditional-opera', label: '传统戏剧', desc: '唱念做打', color: '#4A5F7F', bgGradient: 'linear-gradient(135deg, #E8EBF3 0%, #D1D7E8 100%)' },
|
||||
{ key: 'folk-custom', label: '民俗', desc: '礼仪风尚', color: '#C8363D', bgGradient: 'linear-gradient(135deg, #FFEAE7 0%, #FFD6D1 100%)' },
|
||||
{ key: 'traditional-medicine', label: '传统医药', desc: '岐黄之术', color: '#2A5E4D', bgGradient: 'linear-gradient(135deg, #E6F4F1 0%, #CCE8E1 100%)' },
|
||||
{ key: 'folk-literature', label: '民间文学', desc: '口传心授', color: '#D4A574', bgGradient: 'linear-gradient(135deg, #FFF4E6 0%, #FFE7C8 100%)' },
|
||||
{ key: 'traditional-dance', label: '传统舞蹈', desc: '翩若惊鸿', color: '#4A5F7F', bgGradient: 'linear-gradient(135deg, #E8EBF3 0%, #D1D7E8 100%)' },
|
||||
]
|
||||
|
||||
const heroSlides = [
|
||||
{
|
||||
image: 'https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=1600',
|
||||
title: '传承千年技艺',
|
||||
subtitle: '守护中华文化根脉',
|
||||
},
|
||||
{
|
||||
image: 'https://images.unsplash.com/photo-1617038260897-41a1f14a8ca0?w=1600',
|
||||
title: '匠心独运',
|
||||
subtitle: '让非遗文化重焕新生',
|
||||
},
|
||||
{
|
||||
image: 'https://images.unsplash.com/photo-1544947950-fa07a98d237f?w=1600',
|
||||
title: '文化自信',
|
||||
subtitle: '弘扬中华优秀传统文化',
|
||||
},
|
||||
]
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const [statistics, setStatistics] = useState<Statistics | null>(null)
|
||||
const [featuredHeritage, setFeaturedHeritage] = useState<HeritageItem[]>([])
|
||||
const [featuredInheritors, setFeaturedInheritors] = useState<Inheritor[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [stats, heritage, inheritors] = await Promise.all([
|
||||
getStatistics(),
|
||||
getFeaturedHeritage(6),
|
||||
getFeaturedInheritors(4),
|
||||
])
|
||||
setStatistics(stats)
|
||||
setFeaturedHeritage(heritage)
|
||||
setFeaturedInheritors(inheritors)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="home-page">
|
||||
{/* Hero Banner */}
|
||||
<section className="hero-section">
|
||||
<Carousel autoplay autoplaySpeed={5000} effect="fade" className="hero-carousel">
|
||||
{heroSlides.map((slide, index) => (
|
||||
<div key={index} className="hero-slide">
|
||||
<div
|
||||
className="hero-background"
|
||||
style={{ backgroundImage: `url(${slide.image})` }}
|
||||
/>
|
||||
<div className="hero-overlay" />
|
||||
<div className="hero-content">
|
||||
<div className="container">
|
||||
<h1 className="hero-title animate-fade-in-up">{slide.title}</h1>
|
||||
<p className="hero-subtitle animate-fade-in-up">{slide.subtitle}</p>
|
||||
<Space size="large" className="hero-actions animate-fade-in-up">
|
||||
<Button type="primary" size="large" href="#featured">
|
||||
探索非遗
|
||||
</Button>
|
||||
<Button size="large" href="/heritage">
|
||||
查看全部
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</section>
|
||||
|
||||
{/* 核心价值 */}
|
||||
<section className="values-section section-spacing">
|
||||
<div className="container">
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="value-card">
|
||||
<div className="value-icon-wrapper">
|
||||
<div className="value-icon" style={{ '--icon-color': '#C8363D' } as React.CSSProperties}>
|
||||
<div className="icon-text">传</div>
|
||||
</div>
|
||||
<div className="value-decoration"></div>
|
||||
</div>
|
||||
<h3>传承千年</h3>
|
||||
<div className="value-divider"></div>
|
||||
<p>保护和传承中华民族优秀传统文化,让历史技艺得以延续</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="value-card">
|
||||
<div className="value-icon-wrapper">
|
||||
<div className="value-icon" style={{ '--icon-color': '#D4A574' } as React.CSSProperties}>
|
||||
<div className="icon-text">匠</div>
|
||||
</div>
|
||||
<div className="value-decoration"></div>
|
||||
</div>
|
||||
<h3>匠心独运</h3>
|
||||
<div className="value-divider"></div>
|
||||
<p>弘扬工匠精神,展现非遗项目的精湛技艺和艺术价值</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<div className="value-card">
|
||||
<div className="value-icon-wrapper">
|
||||
<div className="value-icon" style={{ '--icon-color': '#2A5E4D' } as React.CSSProperties}>
|
||||
<div className="icon-text">活</div>
|
||||
</div>
|
||||
<div className="value-decoration"></div>
|
||||
</div>
|
||||
<h3>活态传承</h3>
|
||||
<div className="value-divider"></div>
|
||||
<p>创新传承方式,让非遗文化在当代社会焕发新的生命力</p>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 分类导航 */}
|
||||
<section className="categories-section section-spacing-sm">
|
||||
<div className="container">
|
||||
<div className="section-header text-center">
|
||||
<Title level={2}>非遗分类</Title>
|
||||
<Paragraph>探索丰富多彩的中华非物质文化遗产</Paragraph>
|
||||
</div>
|
||||
<Row gutter={[24, 24]}>
|
||||
{categories.map((category) => (
|
||||
<Col key={category.key} xs={12} sm={12} md={6} lg={6}>
|
||||
<Link
|
||||
to={`/heritage/categories/${category.key}`}
|
||||
className="category-card"
|
||||
style={{
|
||||
'--category-color': category.color,
|
||||
'--category-bg': category.bgGradient,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<div className="category-content">
|
||||
<div className="category-title">{category.label}</div>
|
||||
<div className="category-desc">{category.desc}</div>
|
||||
</div>
|
||||
<div className="category-corner"></div>
|
||||
<div className="category-pattern"></div>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 精选非遗项目 */}
|
||||
<section id="featured" className="featured-section section-spacing">
|
||||
<div className="container">
|
||||
<div className="section-header">
|
||||
<Title level={2}>精选项目</Title>
|
||||
<Link to="/heritage">
|
||||
查看全部 <ArrowRightOutlined />
|
||||
</Link>
|
||||
</div>
|
||||
<Row gutter={[24, 24]}>
|
||||
{featuredHeritage.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={8}>
|
||||
<HeritageCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 代表性传承人 */}
|
||||
<section className="inheritors-section section-spacing gradient-bg-warm">
|
||||
<div className="container">
|
||||
<div className="section-header text-center">
|
||||
<Title level={2}>代表性传承人</Title>
|
||||
<Paragraph>致敬守护文化根脉的匠人大师</Paragraph>
|
||||
</div>
|
||||
<Row gutter={[24, 24]}>
|
||||
{featuredInheritors.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={6}>
|
||||
<InheritorCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<div className="text-center" style={{ marginTop: 40 }}>
|
||||
<Link to="/inheritors">
|
||||
<Button type="primary" size="large">
|
||||
查看更多传承人 <ArrowRightOutlined />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 统计数据 */}
|
||||
{statistics && (
|
||||
<section className="statistics-section section-spacing">
|
||||
<div className="container">
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col xs={12} sm={6}>
|
||||
<div className="stat-card">
|
||||
<Statistic
|
||||
title="非遗项目"
|
||||
value={statistics.totalHeritageItems}
|
||||
prefix={<GlobalOutlined />}
|
||||
suffix="项"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<div className="stat-card">
|
||||
<Statistic
|
||||
title="传承人"
|
||||
value={statistics.totalInheritors}
|
||||
prefix={<TeamOutlined />}
|
||||
suffix="位"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<div className="stat-card">
|
||||
<Statistic
|
||||
title="世界级遗产"
|
||||
value={statistics.worldHeritage}
|
||||
prefix={<TrophyOutlined />}
|
||||
suffix="项"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<div className="stat-card">
|
||||
<Statistic
|
||||
title="覆盖省份"
|
||||
value={statistics.totalProvinces}
|
||||
prefix={<EnvironmentOutlined />}
|
||||
suffix="个"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA 区块 */}
|
||||
<section className="cta-section section-spacing gradient-bg-primary pattern-wave">
|
||||
<div className="container text-center">
|
||||
<Title level={2} style={{ color: '#ffffff', marginBottom: 16 }}>
|
||||
加入我们,共同守护文化遗产
|
||||
</Title>
|
||||
<Paragraph style={{ color: 'rgba(255,255,255,0.9)', fontSize: 16, marginBottom: 32 }}>
|
||||
无论您是传承人、爱好者还是研究者,都可以参与到非遗保护与传承中来
|
||||
</Paragraph>
|
||||
<Space size="large">
|
||||
<Link to="/learn">
|
||||
<Button type="default" size="large" ghost>
|
||||
开始学习
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/about">
|
||||
<Button size="large" style={{ background: '#fff', color: '#C8363D' }}>
|
||||
了解更多
|
||||
</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
276
src/pages/Inheritor/Detail.css
Normal file
276
src/pages/Inheritor/Detail.css
Normal file
@ -0,0 +1,276 @@
|
||||
/* 传承人详情页样式 */
|
||||
|
||||
.inheritor-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.page-breadcrumb {
|
||||
padding: 24px 0;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
/* 传承人信息头部 */
|
||||
.detail-header {
|
||||
margin-bottom: 32px;
|
||||
background: #ffffff;
|
||||
padding: 32px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.inheritor-profile {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.inheritor-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inheritor-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.inheritor-title {
|
||||
font-size: 16px;
|
||||
color: #d4a574;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-meta .anticon {
|
||||
margin-right: 6px;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.detail-stats .anticon {
|
||||
margin-right: 6px;
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
/* 详情卡片区块 */
|
||||
.detail-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* 作品展示 */
|
||||
.detail-gallery {
|
||||
margin-bottom: 32px;
|
||||
background: #ffffff;
|
||||
padding: 32px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.detail-gallery h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 24px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.detail-gallery .ant-image-preview-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-gallery .ant-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 详细内容标签页 */
|
||||
.detail-tabs {
|
||||
margin-bottom: 40px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.detail-text p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 获奖记录 */
|
||||
.awards-section {
|
||||
margin-top: 32px;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.awards-section h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.awards-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.awards-list li {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f0e8;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.awards-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.detail-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sidebar-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-info {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sidebar-info h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 80px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #2c2c2c;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sidebar-card .ant-card-head {
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.sidebar-card .ant-card-head-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 992px) {
|
||||
.detail-sidebar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.inheritor-profile {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.detail-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-gallery {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.inheritor-avatar {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.detail-gallery .ant-image-preview-group {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
366
src/pages/Inheritor/Detail.tsx
Normal file
366
src/pages/Inheritor/Detail.tsx
Normal file
@ -0,0 +1,366 @@
|
||||
/**
|
||||
* 传承人详情页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Avatar, Card, Typography } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
EnvironmentOutlined,
|
||||
EyeOutlined,
|
||||
TrophyOutlined,
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
ManOutlined,
|
||||
WomanOutlined,
|
||||
TeamOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { Inheritor } from '@types/index'
|
||||
import { getInheritorById } from '@services/api'
|
||||
import './Detail.css'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
const levelLabels: Record<string, string> = {
|
||||
national: '国家级',
|
||||
provincial: '省级',
|
||||
municipal: '市级',
|
||||
}
|
||||
|
||||
const InheritorDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<Inheritor | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchData(id)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchData = async (inheritorId: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getInheritorById(inheritorId)
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch inheritor detail:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="empty-container" style={{ padding: '100px 20px', textAlign: 'center' }}>
|
||||
<Empty description="未找到相关传承人" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const age = new Date().getFullYear() - data.birthYear
|
||||
|
||||
return (
|
||||
<div className="inheritor-detail-page">
|
||||
{/* 面包屑导航 */}
|
||||
<div className="page-breadcrumb">
|
||||
<div className="container">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/">
|
||||
<HomeOutlined /> 首页
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/inheritors">传承人</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{data.name}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 封面图 */}
|
||||
{data.coverImage && (
|
||||
<div className="detail-cover" style={{ backgroundImage: `url(${data.coverImage})` }}>
|
||||
<div className="cover-overlay"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详情内容 */}
|
||||
<div className="detail-content section-spacing-sm">
|
||||
<div className="container">
|
||||
<Row gutter={[40, 40]}>
|
||||
<Col xs={24} lg={16}>
|
||||
{/* 头部信息 */}
|
||||
<div className="detail-header">
|
||||
<div className="inheritor-profile">
|
||||
<Avatar size={120} src={data.avatar} icon={<UserOutlined />} className="inheritor-avatar" />
|
||||
<div className="inheritor-info">
|
||||
<Title level={1} className="detail-title">
|
||||
{data.name}
|
||||
</Title>
|
||||
<Text className="inheritor-title">{data.title}</Text>
|
||||
<div className="detail-meta">
|
||||
<Tag color="red">{levelLabels[data.level]}传承人</Tag>
|
||||
<span>
|
||||
<EnvironmentOutlined /> {data.province} {data.city && `· ${data.city}`}
|
||||
</span>
|
||||
<span>
|
||||
{data.gender === 'male' ? <ManOutlined /> : <WomanOutlined />}
|
||||
{data.gender === 'male' ? '男' : '女'}
|
||||
</span>
|
||||
<span>
|
||||
<CalendarOutlined /> {age}岁
|
||||
</span>
|
||||
</div>
|
||||
<div className="detail-stats">
|
||||
<span>
|
||||
<EyeOutlined /> {data.viewCount.toLocaleString()} 浏览
|
||||
</span>
|
||||
<span>
|
||||
<TeamOutlined /> {data.followers.toLocaleString()} 关注
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 个人简介 */}
|
||||
<Card title="个人简介" className="detail-section">
|
||||
<Paragraph style={{ fontSize: 15, lineHeight: 1.8, color: '#666' }}>
|
||||
{data.bio}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
{/* 技艺特点 */}
|
||||
<Card title="技艺特点" className="detail-section">
|
||||
<Paragraph style={{ fontSize: 15, lineHeight: 1.8, color: '#666' }}>
|
||||
{data.masterSkills}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
{/* 成就荣誉 */}
|
||||
{data.achievements && data.achievements.length > 0 && (
|
||||
<Card title="成就荣誉" className="detail-section">
|
||||
<div className="achievements-grid">
|
||||
{data.achievements.map((achievement) => (
|
||||
<div key={achievement.id} className="achievement-item">
|
||||
<div className="achievement-header">
|
||||
<TrophyOutlined style={{ color: '#d4a574', fontSize: 20 }} />
|
||||
<Title level={5} style={{ margin: '0 0 8px 0' }}>
|
||||
{achievement.title}
|
||||
</Title>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{achievement.date}
|
||||
</Text>
|
||||
<Paragraph style={{ marginTop: 12, fontSize: 14, color: '#666' }}>
|
||||
{achievement.description}
|
||||
</Paragraph>
|
||||
{achievement.images && achievement.images.length > 0 && (
|
||||
<Image.PreviewGroup>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
{achievement.images.map((img, idx) => (
|
||||
<Image key={idx} src={img} width={100} style={{ borderRadius: 8 }} />
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 获奖记录 */}
|
||||
{data.awards && data.awards.length > 0 && (
|
||||
<Card title="获奖记录" className="detail-section">
|
||||
<div className="awards-list">
|
||||
{data.awards.map((award) => (
|
||||
<div key={award.id} className="award-item">
|
||||
<TrophyOutlined style={{ color: '#d4a574', marginRight: 12 }} />
|
||||
<div className="award-info">
|
||||
<Text strong>{award.name}</Text>
|
||||
<Text type="secondary" style={{ display: 'block', fontSize: 13 }}>
|
||||
{award.organization} · {award.year}年 · {award.level}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 代表作品 */}
|
||||
{data.works && data.works.length > 0 && (
|
||||
<Card title="代表作品" className="detail-section">
|
||||
<div className="works-grid">
|
||||
{data.works.map((work) => (
|
||||
<div key={work.id} className="work-item">
|
||||
<Image src={work.image} style={{ borderRadius: 8 }} />
|
||||
<Title level={5} style={{ marginTop: 12 }}>
|
||||
{work.name}
|
||||
</Title>
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ fontSize: 13, color: '#666' }}>
|
||||
{work.description}
|
||||
</Paragraph>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
创作于 {work.year}年
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 相关视频 */}
|
||||
{data.videos && data.videos.length > 0 && (
|
||||
<Card title="相关视频" className="detail-section">
|
||||
<div className="videos-grid">
|
||||
{data.videos.map((video) => (
|
||||
<div key={video.id} className="video-item">
|
||||
<div className="video-cover" style={{ backgroundImage: `url(${video.cover})` }}>
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#fff' }} />
|
||||
</div>
|
||||
<Title level={5} style={{ marginTop: 12 }}>
|
||||
{video.title}
|
||||
</Title>
|
||||
{video.description && (
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ fontSize: 13, color: '#666' }}>
|
||||
{video.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#999' }}>
|
||||
<span>
|
||||
<EyeOutlined /> {video.viewCount.toLocaleString()}
|
||||
</span>
|
||||
<span>{Math.floor(video.duration / 60)}:{(video.duration % 60).toString().padStart(2, '0')}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<div className="detail-sidebar">
|
||||
{/* 基本信息 */}
|
||||
<Card title="基本信息" className="sidebar-card">
|
||||
<div className="info-item">
|
||||
<span className="info-label">姓名:</span>
|
||||
<span className="info-value">{data.name}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">性别:</span>
|
||||
<span className="info-value">{data.gender === 'male' ? '男' : '女'}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">年龄:</span>
|
||||
<span className="info-value">{age}岁</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">级别:</span>
|
||||
<span className="info-value">{levelLabels[data.level]}传承人</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">地区:</span>
|
||||
<span className="info-value">
|
||||
{data.province}
|
||||
{data.city && ` · ${data.city}`}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 联系方式 */}
|
||||
{data.contactInfo && (
|
||||
<Card title="联系方式" className="sidebar-card">
|
||||
{data.contactInfo.phone && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">电话:</span>
|
||||
<span className="info-value">{data.contactInfo.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.contactInfo.email && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">邮箱:</span>
|
||||
<span className="info-value">{data.contactInfo.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.contactInfo.address && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">地址:</span>
|
||||
<span className="info-value">{data.contactInfo.address}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.contactInfo.website && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">网站:</span>
|
||||
<span className="info-value">
|
||||
<a href={data.contactInfo.website} target="_blank" rel="noopener noreferrer">
|
||||
{data.contactInfo.website}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 社交媒体 */}
|
||||
{data.socialMedia && (
|
||||
<Card title="社交媒体" className="sidebar-card">
|
||||
{data.socialMedia.weibo && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">微博:</span>
|
||||
<span className="info-value">{data.socialMedia.weibo}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.socialMedia.wechat && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">微信:</span>
|
||||
<span className="info-value">{data.socialMedia.wechat}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.socialMedia.douyin && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">抖音:</span>
|
||||
<span className="info-value">{data.socialMedia.douyin}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.socialMedia.bilibili && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">B站:</span>
|
||||
<span className="info-value">{data.socialMedia.bilibili}</span>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 技艺传承 */}
|
||||
<Card title="技艺传承" className="sidebar-card">
|
||||
<Paragraph style={{ color: '#666', fontSize: 14, lineHeight: 1.6, margin: 0 }}>
|
||||
如果您对该传承人的技艺感兴趣,可以通过联系方式咨询学习事宜,共同传承非遗文化。
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InheritorDetail
|
||||
58
src/pages/Inheritors/List.css
Normal file
58
src/pages/Inheritors/List.css
Normal file
@ -0,0 +1,58 @@
|
||||
/* 传承人列表页样式 */
|
||||
|
||||
.inheritors-list-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #2a5e4d 0%, #1a4a3a 100%);
|
||||
padding: 80px 0 60px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
89
src/pages/Inheritors/List.tsx
Normal file
89
src/pages/Inheritors/List.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 传承人列表页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Row, Col, Spin, Empty } from 'antd'
|
||||
import InheritorCard from '@components/InheritorCard'
|
||||
import CustomPagination from '@components/CustomPagination'
|
||||
import { getInheritorList } from '@services/api'
|
||||
import type { Inheritor, PaginationResult } from '@types/index'
|
||||
import './List.css'
|
||||
|
||||
const InheritorsList: React.FC = () => {
|
||||
const [data, setData] = useState<PaginationResult<Inheritor> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(12)
|
||||
|
||||
const fetchData = async (page: number, size: number) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getInheritorList({ page, pageSize: size })
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch inheritors list:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(currentPage, pageSize)
|
||||
}, [currentPage, pageSize])
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
setCurrentPage(page)
|
||||
setPageSize(size)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleShowSizeChange = (current: number, size: number) => {
|
||||
setCurrentPage(1)
|
||||
setPageSize(size)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inheritors-list-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>代表性传承人</h1>
|
||||
<p>致敬守护文化根脉的匠人大师</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content section-spacing">
|
||||
<div className="container">
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : data && data.data.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{data.data.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={6}>
|
||||
<InheritorCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<CustomPagination
|
||||
current={data.page}
|
||||
total={data.total}
|
||||
pageSize={data.pageSize}
|
||||
onChange={handlePageChange}
|
||||
onShowSizeChange={handleShowSizeChange}
|
||||
unit="位传承人"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无数据" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InheritorsList
|
||||
133
src/pages/Search/index.css
Normal file
133
src/pages/Search/index.css
Normal file
@ -0,0 +1,133 @@
|
||||
/* 搜索页样式 */
|
||||
|
||||
.search-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
background: linear-gradient(135deg, #d4a574 0%, #c8a060 100%);
|
||||
padding: 80px 0 60px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.search-header h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 32px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: 600px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.search-input .ant-input-wrapper {
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.search-input .ant-btn {
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: #c8363d;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-input .ant-btn:hover {
|
||||
background: #a82f35;
|
||||
}
|
||||
|
||||
.search-stats {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-stats strong {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 搜索结果 */
|
||||
.search-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.search-content .ant-tabs {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.result-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-section h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #d4a574;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 66.67%;
|
||||
overflow: hidden;
|
||||
background: #f5f0e8;
|
||||
}
|
||||
|
||||
.card-cover img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.search-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.search-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.search-header h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.result-section h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
266
src/pages/Search/index.tsx
Normal file
266
src/pages/Search/index.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 搜索页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
import { Input, Tabs, List, Card, Tag, Empty, Spin, Row, Col, Avatar } from 'antd'
|
||||
import {
|
||||
SearchOutlined,
|
||||
EnvironmentOutlined,
|
||||
UserOutlined,
|
||||
BookOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { SearchResults } from '@types/index'
|
||||
import { searchAll } from '@services/api'
|
||||
import './index.css'
|
||||
|
||||
const { Search } = Input
|
||||
const { TabPane } = Tabs
|
||||
|
||||
const SearchPage: React.FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const keyword = searchParams.get('q') || ''
|
||||
const [searchKeyword, setSearchKeyword] = useState(keyword)
|
||||
const [results, setResults] = useState<SearchResults | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
setSearchKeyword(keyword)
|
||||
handleSearch(keyword)
|
||||
}
|
||||
}, [keyword])
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
if (!value.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await searchAll(value)
|
||||
setResults(result)
|
||||
setSearchParams({ q: value })
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getTotalCount = () => {
|
||||
if (!results) return 0
|
||||
return (
|
||||
results.heritages.length +
|
||||
results.inheritors.length +
|
||||
results.news.length
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-page">
|
||||
<div className="search-header">
|
||||
<div className="container">
|
||||
<h1>搜索</h1>
|
||||
<Search
|
||||
placeholder="搜索非遗项目、传承人、课程..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
size="large"
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
className="search-input"
|
||||
/>
|
||||
{results && (
|
||||
<p className="search-stats">
|
||||
为您找到 <strong>{getTotalCount()}</strong> 条相关结果
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-content section-spacing-sm">
|
||||
<div className="container">
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : results ? (
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane
|
||||
tab={`全部 (${getTotalCount()})`}
|
||||
key="all"
|
||||
>
|
||||
{getTotalCount() > 0 ? (
|
||||
<div className="search-results-all">
|
||||
{/* 非遗项目 */}
|
||||
{results.heritages.length > 0 && (
|
||||
<div className="result-section">
|
||||
<h2>非遗项目 ({results.heritages.length})</h2>
|
||||
<Row gutter={[24, 24]}>
|
||||
{results.heritages.slice(0, 3).map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={8}>
|
||||
<Link to={`/heritage/${item.id}`}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<div className="card-cover">
|
||||
<img alt={item.name} src={item.images[0]} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Card.Meta
|
||||
title={item.name}
|
||||
description={
|
||||
<div>
|
||||
<Tag color="red">{item.level}</Tag>
|
||||
<Tag>{item.category}</Tag>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 传承人 */}
|
||||
{results.inheritors.length > 0 && (
|
||||
<div className="result-section">
|
||||
<h2>传承人 ({results.inheritors.length})</h2>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={results.inheritors.slice(0, 3)}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar size={64} src={item.avatar} />}
|
||||
title={<Link to={`/inheritor/${item.id}`}>{item.name}</Link>}
|
||||
description={
|
||||
<div>
|
||||
<p>{item.title}</p>
|
||||
<div>
|
||||
<Tag>{item.category}</Tag>
|
||||
<Tag color="red">{item.level}</Tag>
|
||||
<span style={{ marginLeft: 12, color: '#999' }}>
|
||||
<EnvironmentOutlined /> {item.province}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 新闻 */}
|
||||
{results.news.length > 0 && (
|
||||
<div className="result-section">
|
||||
<h2>新闻资讯 ({results.news.length})</h2>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={results.news.slice(0, 3)}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<FileTextOutlined style={{ fontSize: 32, color: '#d4a574' }} />}
|
||||
title={<Link to={`/news/${item.id}`}>{item.title}</Link>}
|
||||
description={item.summary}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty description="未找到相关结果" />
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={`非遗项目 (${results.heritages.length})`}
|
||||
key="heritage"
|
||||
>
|
||||
{results.heritages.length > 0 ? (
|
||||
<Row gutter={[24, 24]}>
|
||||
{results.heritages.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={8}>
|
||||
<Link to={`/heritage/${item.id}`}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<div className="card-cover">
|
||||
<img alt={item.name} src={item.images[0]} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Card.Meta
|
||||
title={item.name}
|
||||
description={
|
||||
<div>
|
||||
<Tag color="red">{item.level}</Tag>
|
||||
<Tag>{item.category}</Tag>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description="未找到相关非遗项目" />
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={`传承人 (${results.inheritors.length})`}
|
||||
key="inheritor"
|
||||
>
|
||||
{results.inheritors.length > 0 ? (
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={results.inheritors}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar size={64} src={item.avatar} />}
|
||||
title={<Link to={`/inheritor/${item.id}`}>{item.name}</Link>}
|
||||
description={
|
||||
<div>
|
||||
<p>{item.title}</p>
|
||||
<div>
|
||||
<Tag>{item.category}</Tag>
|
||||
<Tag color="red">{item.level}</Tag>
|
||||
<span style={{ marginLeft: 12, color: '#999' }}>
|
||||
<EnvironmentOutlined /> {item.province}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="未找到相关传承人" />
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
) : (
|
||||
<Empty description="请输入搜索关键词" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchPage
|
||||
172
src/pages/User/Center.css
Normal file
172
src/pages/User/Center.css
Normal file
@ -0,0 +1,172 @@
|
||||
/* 用户中心页样式 */
|
||||
|
||||
.user-center-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #d4a574 0%, #c8a060 100%);
|
||||
padding: 60px 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-profile h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-actions button {
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #ffffff;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.user-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 32px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.user-stats .ant-statistic {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-stats .ant-statistic-title {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-stats .ant-statistic-content {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-stats .anticon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 页面内容 */
|
||||
.page-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.page-content .ant-tabs {
|
||||
background: #ffffff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 卡片封面 */
|
||||
.card-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 66.67%;
|
||||
overflow: hidden;
|
||||
background: #f5f0e8;
|
||||
}
|
||||
|
||||
.card-cover img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 成就网格 */
|
||||
.achievements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.achievement-card {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.achievement-card.locked {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.achievement-card:not(.locked):hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.achievement-icon {
|
||||
font-size: 48px;
|
||||
color: #d4a574;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.achievement-card h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.achievement-card p {
|
||||
font-size: 13px;
|
||||
color: #999999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 992px) {
|
||||
.user-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.page-header {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.achievements-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
173
src/pages/User/Center.tsx
Normal file
173
src/pages/User/Center.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 用户中心页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Tabs, Card, Avatar, Button, List, Empty, Tag, Row, Col, Statistic } from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
HeartOutlined,
|
||||
BookOutlined,
|
||||
CommentOutlined,
|
||||
EditOutlined,
|
||||
TrophyOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useUserStore } from '@store/useUserStore'
|
||||
import type { HeritageItem } from '@types/index'
|
||||
import { getHeritageById } from '@services/api'
|
||||
import './Center.css'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
const UserCenter: React.FC = () => {
|
||||
const { user, isAuthenticated, logout } = useUserStore()
|
||||
const navigate = useNavigate()
|
||||
const [favorites, setFavorites] = useState<HeritageItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
fetchUserData()
|
||||
}, [isAuthenticated])
|
||||
|
||||
const fetchUserData = async () => {
|
||||
if (!user) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// 获取收藏的非遗项目
|
||||
const favoriteItems = await Promise.all(
|
||||
user.favorites.slice(0, 6).map((id) => getHeritageById(id))
|
||||
)
|
||||
setFavorites(favoriteItems)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Empty description="未登录" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-center-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col xs={24} md={8}>
|
||||
<div className="user-profile">
|
||||
<Avatar size={100} src={user.avatar} icon={<UserOutlined />} />
|
||||
<h2>{user.username}</h2>
|
||||
<p className="user-email">{user.email}</p>
|
||||
<div className="user-actions">
|
||||
<Button icon={<EditOutlined />}>编辑资料</Button>
|
||||
<Button onClick={handleLogout}>退出登录</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={16}>
|
||||
<div className="user-stats">
|
||||
<Statistic
|
||||
title="收藏数"
|
||||
value={user.favorites.length}
|
||||
prefix={<HeartOutlined />}
|
||||
/>
|
||||
<Statistic
|
||||
title="评论数"
|
||||
value={0}
|
||||
prefix={<CommentOutlined />}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content section-spacing-sm">
|
||||
<div className="container">
|
||||
<Tabs defaultActiveKey="favorites">
|
||||
<TabPane tab="我的收藏" key="favorites">
|
||||
{favorites.length > 0 ? (
|
||||
<Row gutter={[24, 24]}>
|
||||
{favorites.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} lg={8}>
|
||||
<Link to={`/heritage/${item.id}`}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<div className="card-cover">
|
||||
<img alt={item.name} src={item.images[0]} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Card.Meta
|
||||
title={item.name}
|
||||
description={
|
||||
<div>
|
||||
<Tag color="red">{item.level}</Tag>
|
||||
<Tag>{item.category}</Tag>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description="暂无收藏" />
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="我的评论" key="comments">
|
||||
<Empty description="暂无评论" />
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="学习成就" key="achievements">
|
||||
<div className="achievements-grid">
|
||||
<Card className="achievement-card locked">
|
||||
<TrophyOutlined className="achievement-icon" />
|
||||
<h3>文化探索者</h3>
|
||||
<p>收藏第一个非遗项目</p>
|
||||
</Card>
|
||||
<Card className="achievement-card locked">
|
||||
<TrophyOutlined className="achievement-icon" />
|
||||
<h3>热心评论家</h3>
|
||||
<p>发表10条评论</p>
|
||||
</Card>
|
||||
<Card className="achievement-card locked">
|
||||
<TrophyOutlined className="achievement-icon" />
|
||||
<h3>非遗达人</h3>
|
||||
<p>收藏50个非遗项目</p>
|
||||
</Card>
|
||||
<Card className="achievement-card locked">
|
||||
<TrophyOutlined className="achievement-icon" />
|
||||
<h3>文化使者</h3>
|
||||
<p>分享5次非遗知识</p>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserCenter
|
||||
180
src/pages/User/Login.css
Normal file
180
src/pages/User/Login.css
Normal file
@ -0,0 +1,180 @@
|
||||
/* 用户登录页样式 */
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f5f0e8 0%, #ffffff 100%);
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 60px 50px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
color: #d4a574;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: #c8a060;
|
||||
}
|
||||
|
||||
.social-login {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.social-btn {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.social-btn.wechat {
|
||||
color: #07c160;
|
||||
border-color: #07c160;
|
||||
}
|
||||
|
||||
.social-btn.wechat:hover {
|
||||
color: #ffffff;
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
}
|
||||
|
||||
.social-btn.alipay {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.social-btn.alipay:hover {
|
||||
color: #ffffff;
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.register-link a {
|
||||
color: #d4a574;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.register-link a:hover {
|
||||
color: #c8a060;
|
||||
}
|
||||
|
||||
/* 插图区域 */
|
||||
.login-illustration {
|
||||
background: linear-gradient(135deg, #d4a574 0%, #c8a060 100%);
|
||||
padding: 60px 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.illustration-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.illustration-content h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
color: #ffffff;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.illustration-content p {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.illustration-image {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.illustration-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 992px) {
|
||||
.login-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.login-illustration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.login-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.social-login {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
107
src/pages/User/Login.tsx
Normal file
107
src/pages/User/Login.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 用户登录页
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Form, Input, Button, message, Divider } from 'antd'
|
||||
import { UserOutlined, LockOutlined, WechatOutlined, AlipayOutlined } from '@ant-design/icons'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useUserStore } from '@store/useUserStore'
|
||||
import './Login.css'
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const login = useUserStore((state) => state.login)
|
||||
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await login(values.username, values.password)
|
||||
message.success('登录成功')
|
||||
navigate('/')
|
||||
} catch (error) {
|
||||
message.error('登录失败,请检查用户名或密码')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-container">
|
||||
<div className="login-card">
|
||||
<div className="login-header">
|
||||
<h1>欢迎回来</h1>
|
||||
<p>登录以继续您的非遗文化之旅</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div className="login-options">
|
||||
<Link to="/forgot-password" className="forgot-link">
|
||||
忘记密码?
|
||||
</Link>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider plain>其他登录方式</Divider>
|
||||
|
||||
<div className="social-login">
|
||||
<Button icon={<WechatOutlined />} className="social-btn wechat">
|
||||
微信登录
|
||||
</Button>
|
||||
<Button icon={<AlipayOutlined />} className="social-btn alipay">
|
||||
支付宝登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="register-link">
|
||||
还没有账号?<Link to="/register">立即注册</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-illustration">
|
||||
<div className="illustration-content">
|
||||
<h2>传承非遗文化</h2>
|
||||
<p>探索中华文化瑰宝,传承千年技艺精髓</p>
|
||||
<div className="illustration-image">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1547618707-5e8b2c1f0f7c?w=600"
|
||||
alt="传统文化"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
146
src/pages/User/Register.css
Normal file
146
src/pages/User/Register.css
Normal file
@ -0,0 +1,146 @@
|
||||
/* 用户注册页样式 */
|
||||
|
||||
.register-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f5f0e8 0%, #ffffff 100%);
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.register-container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
padding: 60px 50px;
|
||||
}
|
||||
|
||||
.register-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.register-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #2c2c2c;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.register-header p {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.login-link a {
|
||||
color: #d4a574;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.login-link a:hover {
|
||||
color: #c8a060;
|
||||
}
|
||||
|
||||
/* 插图区域 */
|
||||
.register-illustration {
|
||||
background: linear-gradient(135deg, #c8363d 0%, #a82f35 100%);
|
||||
padding: 60px 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.illustration-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.illustration-content h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
color: #ffffff;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.illustration-content > p {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
text-align: left;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-item h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 992px) {
|
||||
.register-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.register-illustration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.register-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.register-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
167
src/pages/User/Register.tsx
Normal file
167
src/pages/User/Register.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 用户注册页
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Form, Input, Button, message, Checkbox } from 'antd'
|
||||
import { UserOutlined, LockOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useUserStore } from '@store/useUserStore'
|
||||
import './Register.css'
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const register = useUserStore((state) => state.register)
|
||||
|
||||
const onFinish = async (values: {
|
||||
username: string
|
||||
email: string
|
||||
phone: string
|
||||
password: string
|
||||
agree: boolean
|
||||
}) => {
|
||||
if (!values.agree) {
|
||||
message.warning('请阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await register(values.username, values.email, values.password)
|
||||
message.success('注册成功,欢迎加入非遗文化传承平台')
|
||||
navigate('/')
|
||||
} catch (error) {
|
||||
message.error('注册失败,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="register-page">
|
||||
<div className="register-container">
|
||||
<div className="register-card">
|
||||
<div className="register-header">
|
||||
<h1>加入我们</h1>
|
||||
<p>开启您的非遗文化传承之旅</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="register"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
{ max: 20, message: '用户名最多20个字符' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<MailOutlined />} placeholder="邮箱" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="phone"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<PhoneOutlined />} placeholder="手机号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'))
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="确认密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="agree"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>
|
||||
我已阅读并同意
|
||||
<Link to="/terms" style={{ margin: '0 4px' }}>用户协议</Link>
|
||||
和
|
||||
<Link to="/privacy" style={{ margin: '0 4px' }}>隐私政策</Link>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
注册
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="login-link">
|
||||
已有账号?<Link to="/login">立即登录</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="register-illustration">
|
||||
<div className="illustration-content">
|
||||
<h2>探索非遗之美</h2>
|
||||
<p>与千万用户一起,传承中华优秀传统文化</p>
|
||||
<div className="feature-list">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">📚</div>
|
||||
<h3>在线学习</h3>
|
||||
<p>海量非遗课程,随时随地学习</p>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">👥</div>
|
||||
<h3>名师指导</h3>
|
||||
<p>跟随传承人学习传统技艺</p>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">🏆</div>
|
||||
<h3>获得认证</h3>
|
||||
<p>完成学习获得专业证书</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
Loading…
Reference in New Issue
Block a user