新增功能:活动资讯页面

- 创建资讯列表页:Tab切换(资讯动态/活动预告),支持分类和状态筛选
- 创建资讯详情页:完整展示文章内容、作者、发布时间、标签等
- 创建活动详情页:展示活动信息、报名状态、主办方联系方式
- 实现分页功能和响应式布局
This commit is contained in:
Leo 2025-10-09 23:48:07 +08:00
parent 6257ce5c7b
commit 9c5cd4da85
6 changed files with 1331 additions and 0 deletions

173
src/pages/News/Detail.css Normal file
View 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
View 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

View 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;
}
}

View 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
View 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
View 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