新增功能:活动资讯页面
- 创建资讯列表页:Tab切换(资讯动态/活动预告),支持分类和状态筛选 - 创建资讯详情页:完整展示文章内容、作者、发布时间、标签等 - 创建活动详情页:展示活动信息、报名状态、主办方联系方式 - 实现分页功能和响应式布局
This commit is contained in:
parent
6257ce5c7b
commit
9c5cd4da85
173
src/pages/News/Detail.css
Normal file
173
src/pages/News/Detail.css
Normal file
@ -0,0 +1,173 @@
|
||||
/* 资讯详情页样式 */
|
||||
|
||||
.news-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
/* 面包屑导航 */
|
||||
.page-breadcrumb {
|
||||
background: #ffffff;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb a {
|
||||
color: #666666;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb a:hover {
|
||||
color: #c8363d;
|
||||
}
|
||||
|
||||
/* 封面图 */
|
||||
.detail-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
|
||||
}
|
||||
|
||||
/* 详情内容 */
|
||||
.detail-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.detail-main {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
padding: 48px 64px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 头部信息 */
|
||||
.detail-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
font-size: 14px;
|
||||
padding: 4px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 36px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #1a1a1a !important;
|
||||
line-height: 1.4 !important;
|
||||
margin-bottom: 12px !important;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
font-size: 18px !important;
|
||||
color: #666666 !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-meta .meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.detail-meta .meta-item .anticon {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 摘要 */
|
||||
.detail-summary {
|
||||
margin: 32px 0;
|
||||
padding: 24px;
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #c8363d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 16px !important;
|
||||
color: #333333 !important;
|
||||
line-height: 1.8 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 正文内容 */
|
||||
.detail-body {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
font-size: 17px !important;
|
||||
color: #333333 !important;
|
||||
line-height: 1.9 !important;
|
||||
text-align: justify;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.detail-tags {
|
||||
margin-top: 40px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-tags .ant-tag {
|
||||
font-size: 13px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.detail-cover {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.detail-main {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.detail-meta .ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
169
src/pages/News/Detail.tsx
Normal file
169
src/pages/News/Detail.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 资讯详情页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { Breadcrumb, Spin, Empty, Tag, Space, Typography, Divider } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
EyeOutlined,
|
||||
HeartOutlined,
|
||||
CalendarOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getNewsById } from '@services/api'
|
||||
import type { NewsArticle } from '@/types'
|
||||
import './Detail.css'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
exhibition: '展览',
|
||||
activity: '活动',
|
||||
policy: '政策',
|
||||
research: '研究',
|
||||
story: '故事',
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
exhibition: 'purple',
|
||||
activity: 'blue',
|
||||
policy: 'red',
|
||||
research: 'green',
|
||||
story: 'orange',
|
||||
}
|
||||
|
||||
const NewsDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<NewsArticle | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchData(id)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchData = async (newsId: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getNewsById(newsId)
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch news 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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="news-detail-page">
|
||||
{/* 面包屑导航 */}
|
||||
<div className="page-breadcrumb">
|
||||
<div className="container">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/">
|
||||
<HomeOutlined /> 首页
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/news">活动资讯</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{data.title}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 封面图 */}
|
||||
{data.cover && (
|
||||
<div className="detail-cover" style={{ backgroundImage: `url(${data.cover})` }}>
|
||||
<div className="cover-overlay"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详情内容 */}
|
||||
<div className="detail-content section-spacing-sm">
|
||||
<div className="container">
|
||||
<div className="detail-main">
|
||||
{/* 头部信息 */}
|
||||
<div className="detail-header">
|
||||
<Tag color={categoryColors[data.category]} className="category-tag">
|
||||
{categoryLabels[data.category]}
|
||||
</Tag>
|
||||
<Title level={1} className="detail-title">
|
||||
{data.title}
|
||||
</Title>
|
||||
{data.subtitle && (
|
||||
<Paragraph className="detail-subtitle">{data.subtitle}</Paragraph>
|
||||
)}
|
||||
<div className="detail-meta">
|
||||
<Space size="large" wrap>
|
||||
<span className="meta-item">
|
||||
<UserOutlined />
|
||||
<Text>作者:{data.author}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<CalendarOutlined />
|
||||
<Text>发布时间:{data.publishDate}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<EyeOutlined />
|
||||
<Text>{data.viewCount.toLocaleString()} 浏览</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<HeartOutlined />
|
||||
<Text>{data.likeCount.toLocaleString()} 点赞</Text>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 摘要 */}
|
||||
<div className="detail-summary">
|
||||
<Paragraph className="summary-text">{data.summary}</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 正文内容 */}
|
||||
<div className="detail-body">
|
||||
<Paragraph className="body-text">{data.content}</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
{data.tags && data.tags.length > 0 && (
|
||||
<div className="detail-tags">
|
||||
<Space size="small" wrap>
|
||||
<Text type="secondary">标签:</Text>
|
||||
{data.tags.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewsDetail
|
||||
285
src/pages/News/EventDetail.css
Normal file
285
src/pages/News/EventDetail.css
Normal file
@ -0,0 +1,285 @@
|
||||
/* 活动详情页样式 */
|
||||
|
||||
.event-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
/* 面包屑导航 */
|
||||
.page-breadcrumb {
|
||||
background: #ffffff;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb a {
|
||||
color: #666666;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.page-breadcrumb .ant-breadcrumb a:hover {
|
||||
color: #c8363d;
|
||||
}
|
||||
|
||||
/* 封面图 */
|
||||
.event-detail-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.4) 100%);
|
||||
}
|
||||
|
||||
.cover-badge {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 16px;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 详情内容 */
|
||||
.event-detail-content {
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.event-detail-main {
|
||||
background: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 头部信息 */
|
||||
.event-detail-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
font-size: 14px;
|
||||
padding: 4px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.event-detail-title {
|
||||
font-size: 36px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #1a1a1a !important;
|
||||
line-height: 1.4 !important;
|
||||
margin-bottom: 24px !important;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.event-detail-meta {
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.event-detail-meta .meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.event-detail-meta .meta-item .anticon {
|
||||
color: #c8363d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 活动描述 */
|
||||
.event-detail-description {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.event-detail-description h3 {
|
||||
font-size: 24px;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 17px !important;
|
||||
color: #333333 !important;
|
||||
line-height: 1.9 !important;
|
||||
text-align: justify;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.event-detail-tags {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.event-detail-tags .ant-tag {
|
||||
font-size: 13px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.event-sidebar {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
/* 报名信息卡片 */
|
||||
.enrollment-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.price-icon {
|
||||
font-size: 32px;
|
||||
color: #c8363d;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.enrollment-info {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.info-row .anticon {
|
||||
color: #c8363d;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.info-row > span:nth-child(2) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.capacity-progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 组织方信息卡片 */
|
||||
.organizer-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.organizer-card h4 {
|
||||
font-size: 18px;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 0;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
}
|
||||
|
||||
.organizer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.contact-item .anticon {
|
||||
color: #999999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 992px) {
|
||||
.event-sidebar {
|
||||
position: static;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.event-detail-cover {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.cover-badge {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
.event-detail-main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.event-detail-title {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
.event-detail-meta .ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.event-detail-description h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
341
src/pages/News/EventDetail.tsx
Normal file
341
src/pages/News/EventDetail.tsx
Normal file
@ -0,0 +1,341 @@
|
||||
/**
|
||||
* 活动详情页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import {
|
||||
Breadcrumb,
|
||||
Spin,
|
||||
Empty,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
EyeOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
EnvironmentOutlined,
|
||||
UserOutlined,
|
||||
DollarOutlined,
|
||||
TeamOutlined,
|
||||
PhoneOutlined,
|
||||
MailOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getEventById } from '@services/api'
|
||||
import type { Event } from '@/types'
|
||||
import './EventDetail.css'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
exhibition: '展览',
|
||||
workshop: '工作坊',
|
||||
performance: '演出',
|
||||
lecture: '讲座',
|
||||
festival: '节日',
|
||||
}
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
exhibition: 'purple',
|
||||
workshop: 'blue',
|
||||
performance: 'red',
|
||||
lecture: 'green',
|
||||
festival: 'orange',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
upcoming: '即将开始',
|
||||
ongoing: '进行中',
|
||||
finished: '已结束',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
upcoming: 'blue',
|
||||
ongoing: 'green',
|
||||
finished: 'default',
|
||||
cancelled: 'red',
|
||||
}
|
||||
|
||||
const EventDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<Event | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchData(id)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchData = async (eventId: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getEventById(eventId)
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event detail:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnroll = () => {
|
||||
// TODO: 实现报名逻辑
|
||||
console.log('报名活动:', id)
|
||||
}
|
||||
|
||||
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 isEnrollable =
|
||||
data.status === 'upcoming' &&
|
||||
(!data.capacity || data.enrolled < data.capacity)
|
||||
|
||||
const isFull = data.capacity && data.enrolled >= data.capacity
|
||||
|
||||
return (
|
||||
<div className="event-detail-page">
|
||||
{/* 面包屑导航 */}
|
||||
<div className="page-breadcrumb">
|
||||
<div className="container">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/">
|
||||
<HomeOutlined /> 首页
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>
|
||||
<Link to="/news">活动资讯</Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>{data.title}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 封面图 */}
|
||||
{data.cover && (
|
||||
<div className="event-detail-cover" style={{ backgroundImage: `url(${data.cover})` }}>
|
||||
<div className="cover-overlay"></div>
|
||||
<div className="cover-badge">
|
||||
<Tag color={statusColors[data.status]} className="status-badge">
|
||||
{statusLabels[data.status]}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详情内容 */}
|
||||
<div className="event-detail-content section-spacing-sm">
|
||||
<div className="container">
|
||||
<Row gutter={32}>
|
||||
{/* 左侧主要内容 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<div className="event-detail-main">
|
||||
{/* 头部信息 */}
|
||||
<div className="event-detail-header">
|
||||
<Tag color={typeColors[data.type]} className="type-tag">
|
||||
{typeLabels[data.type]}
|
||||
</Tag>
|
||||
<Title level={1} className="event-detail-title">
|
||||
{data.title}
|
||||
</Title>
|
||||
|
||||
<div className="event-detail-meta">
|
||||
<Space size="large" wrap>
|
||||
<span className="meta-item">
|
||||
<CalendarOutlined />
|
||||
<Text>
|
||||
{data.startDate}
|
||||
{data.startDate !== data.endDate && ` ~ ${data.endDate}`}
|
||||
</Text>
|
||||
</span>
|
||||
{data.startTime && (
|
||||
<span className="meta-item">
|
||||
<ClockCircleOutlined />
|
||||
<Text>
|
||||
{data.startTime} - {data.endTime}
|
||||
</Text>
|
||||
</span>
|
||||
)}
|
||||
<span className="meta-item">
|
||||
<EnvironmentOutlined />
|
||||
<Text>{data.location}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<EyeOutlined />
|
||||
<Text>{data.viewCount.toLocaleString()} 浏览</Text>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 活动描述 */}
|
||||
<div className="event-detail-description">
|
||||
<Title level={3}>活动介绍</Title>
|
||||
<Paragraph className="description-text">{data.description}</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
{data.tags && data.tags.length > 0 && (
|
||||
<div className="event-detail-tags">
|
||||
<Space size="small" wrap>
|
||||
<Text type="secondary">标签:</Text>
|
||||
{data.tags.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* 右侧信息栏 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<div className="event-sidebar">
|
||||
{/* 报名信息卡片 */}
|
||||
<Card className="enrollment-card" bordered={false}>
|
||||
<div className="price-info">
|
||||
<DollarOutlined className="price-icon" />
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 14 }}>
|
||||
活动费用
|
||||
</Text>
|
||||
<div className="price-value">
|
||||
{data.isFree ? (
|
||||
<Text strong style={{ fontSize: 28, color: '#52c41a' }}>
|
||||
免费
|
||||
</Text>
|
||||
) : (
|
||||
<Text strong style={{ fontSize: 28, color: '#c8363d' }}>
|
||||
¥{data.price}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="enrollment-info">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div className="info-row">
|
||||
<TeamOutlined />
|
||||
<Text>报名人数</Text>
|
||||
<Text strong>
|
||||
{data.enrolled} / {data.capacity || '不限'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{data.capacity && (
|
||||
<div className="capacity-progress">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${(data.enrolled / data.capacity) * 100}%`,
|
||||
backgroundColor: isFull ? '#ff4d4f' : '#52c41a',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFull && (
|
||||
<Tag color="red" style={{ width: '100%', textAlign: 'center' }}>
|
||||
名额已满
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
{data.status === 'finished' && (
|
||||
<Tag color="default" style={{ width: '100%', textAlign: 'center' }}>
|
||||
活动已结束
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
{data.status === 'cancelled' && (
|
||||
<Tag color="red" style={{ width: '100%', textAlign: 'center' }}>
|
||||
活动已取消
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
disabled={!isEnrollable}
|
||||
onClick={handleEnroll}
|
||||
style={{
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{isEnrollable ? '立即报名' : '无法报名'}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 组织方信息 */}
|
||||
{data.organizer && (
|
||||
<Card className="organizer-card" bordered={false}>
|
||||
<Title level={4}>主办方</Title>
|
||||
<Divider style={{ margin: '12px 0 16px' }} />
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div className="organizer-info">
|
||||
<UserOutlined style={{ fontSize: 20, color: '#c8363d' }} />
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
{data.organizer}
|
||||
</Text>
|
||||
</div>
|
||||
{data.contactInfo && (data.contactInfo.phone || data.contactInfo.email) && (
|
||||
<>
|
||||
{data.contactInfo.phone && (
|
||||
<div className="contact-item">
|
||||
<PhoneOutlined />
|
||||
<Text type="secondary">{data.contactInfo.phone}</Text>
|
||||
</div>
|
||||
)}
|
||||
{data.contactInfo.email && (
|
||||
<div className="contact-item">
|
||||
<MailOutlined />
|
||||
<Text type="secondary">{data.contactInfo.email}</Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventDetail
|
||||
111
src/pages/News/index.css
Normal file
111
src/pages/News/index.css
Normal file
@ -0,0 +1,111 @@
|
||||
/* 活动资讯页面样式 */
|
||||
|
||||
.news-page {
|
||||
min-height: 100vh;
|
||||
background: #fafaf8;
|
||||
}
|
||||
|
||||
.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: #fafaf8;
|
||||
}
|
||||
|
||||
/* Tabs样式 */
|
||||
.news-tabs {
|
||||
background: #ffffff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-tabs .ant-tabs-nav {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.news-tabs .ant-tabs-tab {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.news-tabs .ant-tabs-tab .anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-section {
|
||||
background: #f5f5f5;
|
||||
padding: 20px 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-section .ant-select {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* 加载容器 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 60px 0 40px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.news-tabs {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.news-tabs .ant-tabs-tab {
|
||||
font-size: 14px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
252
src/pages/News/index.tsx
Normal file
252
src/pages/News/index.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 活动资讯列表页
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Row, Col, Spin, Empty, Tabs, Select, Space } from 'antd'
|
||||
import { FileTextOutlined, CalendarOutlined } from '@ant-design/icons'
|
||||
import NewsCard from '@components/NewsCard'
|
||||
import EventCard from '@components/EventCard'
|
||||
import CustomPagination from '@components/CustomPagination'
|
||||
import { getNewsList, getEventList } from '@services/api'
|
||||
import type { NewsArticle, Event, PaginationResult } from '@/types'
|
||||
import './index.css'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
// 资讯分类选项
|
||||
const newsCategoryOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '展览', value: 'exhibition' },
|
||||
{ label: '活动', value: 'activity' },
|
||||
{ label: '政策', value: 'policy' },
|
||||
{ label: '研究', value: 'research' },
|
||||
{ label: '故事', value: 'story' },
|
||||
]
|
||||
|
||||
// 活动类型选项
|
||||
const eventTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '展览', value: 'exhibition' },
|
||||
{ label: '工作坊', value: 'workshop' },
|
||||
{ label: '演出', value: 'performance' },
|
||||
{ label: '讲座', value: 'lecture' },
|
||||
{ label: '节日', value: 'festival' },
|
||||
]
|
||||
|
||||
// 活动状态选项
|
||||
const eventStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '即将开始', value: 'upcoming' },
|
||||
{ label: '进行中', value: 'ongoing' },
|
||||
{ label: '已结束', value: 'finished' },
|
||||
]
|
||||
|
||||
const NewsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('news')
|
||||
const [newsData, setNewsData] = useState<PaginationResult<NewsArticle> | null>(null)
|
||||
const [eventsData, setEventsData] = useState<PaginationResult<Event> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 资讯筛选条件
|
||||
const [newsCategory, setNewsCategory] = useState('')
|
||||
const [newsPage, setNewsPage] = useState(1)
|
||||
const [newsPageSize, setNewsPageSize] = useState(12)
|
||||
|
||||
// 活动筛选条件
|
||||
const [eventType, setEventType] = useState('')
|
||||
const [eventStatus, setEventStatus] = useState('')
|
||||
const [eventPage, setEventPage] = useState(1)
|
||||
const [eventPageSize, setEventPageSize] = useState(12)
|
||||
|
||||
// 获取资讯列表
|
||||
const fetchNews = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getNewsList({
|
||||
category: (newsCategory || undefined) as any,
|
||||
page: newsPage,
|
||||
pageSize: newsPageSize,
|
||||
})
|
||||
setNewsData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch news:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取活动列表
|
||||
const fetchEvents = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getEventList({
|
||||
type: eventType || undefined,
|
||||
status: eventStatus || undefined,
|
||||
page: eventPage,
|
||||
pageSize: eventPageSize,
|
||||
})
|
||||
setEventsData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch events:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'news') {
|
||||
fetchNews()
|
||||
} else {
|
||||
fetchEvents()
|
||||
}
|
||||
}, [activeTab, newsCategory, newsPage, newsPageSize, eventType, eventStatus, eventPage, eventPageSize])
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key)
|
||||
}
|
||||
|
||||
const handleNewsPageChange = (page: number, size: number) => {
|
||||
if (size === newsPageSize) {
|
||||
setNewsPage(page)
|
||||
} else {
|
||||
setNewsPage(1)
|
||||
setNewsPageSize(size)
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleEventPageChange = (page: number, size: number) => {
|
||||
if (size === eventPageSize) {
|
||||
setEventPage(page)
|
||||
} else {
|
||||
setEventPage(1)
|
||||
setEventPageSize(size)
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="news-page">
|
||||
<div className="page-header">
|
||||
<div className="container">
|
||||
<h1>活动资讯</h1>
|
||||
<p>了解最新的非遗活动与资讯动态</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content section-spacing">
|
||||
<div className="container-wide">
|
||||
<Tabs activeKey={activeTab} onChange={handleTabChange} size="large" className="news-tabs">
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<FileTextOutlined />
|
||||
资讯动态
|
||||
</span>
|
||||
}
|
||||
key="news"
|
||||
>
|
||||
{/* 资讯筛选 */}
|
||||
<div className="filter-section">
|
||||
<Space size="middle">
|
||||
<span className="filter-label">分类筛选:</span>
|
||||
<Select
|
||||
value={newsCategory}
|
||||
onChange={setNewsCategory}
|
||||
style={{ width: 150 }}
|
||||
options={newsCategoryOptions}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 资讯列表 */}
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : newsData && newsData.data.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{newsData.data.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<NewsCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<CustomPagination
|
||||
current={newsPage}
|
||||
total={newsData.total}
|
||||
pageSize={newsPageSize}
|
||||
onChange={handleNewsPageChange}
|
||||
unit="条"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无资讯" />
|
||||
)}
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<CalendarOutlined />
|
||||
活动预告
|
||||
</span>
|
||||
}
|
||||
key="events"
|
||||
>
|
||||
{/* 活动筛选 */}
|
||||
<div className="filter-section">
|
||||
<Space size="middle" wrap>
|
||||
<span className="filter-label">活动类型:</span>
|
||||
<Select
|
||||
value={eventType}
|
||||
onChange={setEventType}
|
||||
style={{ width: 150 }}
|
||||
options={eventTypeOptions}
|
||||
/>
|
||||
<span className="filter-label">活动状态:</span>
|
||||
<Select
|
||||
value={eventStatus}
|
||||
onChange={setEventStatus}
|
||||
style={{ width: 150 }}
|
||||
options={eventStatusOptions}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 活动列表 */}
|
||||
{loading ? (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : eventsData && eventsData.data.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{eventsData.data.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<EventCard item={item} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<CustomPagination
|
||||
current={eventPage}
|
||||
total={eventsData.total}
|
||||
pageSize={eventPageSize}
|
||||
onChange={handleEventPageChange}
|
||||
unit="个"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无活动" />
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewsPage
|
||||
Loading…
Reference in New Issue
Block a user