feat: 完善活动资讯页面功能
- 集成评论区到详情页 - 添加活动报名功能 - 优化资讯卡片展示 - 改进活动详情页布局和交互
This commit is contained in:
parent
02b4a1eeec
commit
8f603837a1
@ -4,39 +4,40 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { Breadcrumb, Spin, Empty, Tag, Space, Typography, Divider } from 'antd'
|
||||
import { Breadcrumb, Spin, Empty, Tag, Space, Typography, Divider, Card } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
EyeOutlined,
|
||||
HeartOutlined,
|
||||
LikeOutlined,
|
||||
CalendarOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getNewsById } from '@services/api'
|
||||
import type { NewsArticle } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { getNewsDetail } from '@services/newsApi'
|
||||
import FavoriteButton from '@components/FavoriteButton'
|
||||
import LikeButton from '@components/LikeButton'
|
||||
import CommentSection from '@components/CommentSection'
|
||||
import type { NewsDetail } from '@/types'
|
||||
import './Detail.css'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
// 后端分类映射:news、activity、notice
|
||||
const categoryLabels: Record<string, string> = {
|
||||
exhibition: '展览',
|
||||
news: '新闻',
|
||||
activity: '活动',
|
||||
policy: '政策',
|
||||
research: '研究',
|
||||
story: '故事',
|
||||
notice: '通知',
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
exhibition: 'purple',
|
||||
activity: 'blue',
|
||||
policy: 'red',
|
||||
research: 'green',
|
||||
story: 'orange',
|
||||
news: 'blue',
|
||||
activity: 'green',
|
||||
notice: 'red',
|
||||
}
|
||||
|
||||
const NewsDetail: React.FC = () => {
|
||||
const NewsDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [data, setData] = useState<NewsArticle | null>(null)
|
||||
const [data, setData] = useState<NewsDetail | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
@ -48,7 +49,7 @@ const NewsDetail: React.FC = () => {
|
||||
const fetchData = async (newsId: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getNewsById(newsId)
|
||||
const result = await getNewsDetail(newsId)
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch news detail:', error)
|
||||
@ -73,6 +74,9 @@ const NewsDetail: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// 处理标签字符串
|
||||
const tagArray = data?.tags ? data.tags.split(',').filter(tag => tag.trim()) : []
|
||||
|
||||
return (
|
||||
<div className="news-detail-page">
|
||||
{/* 面包屑导航 */}
|
||||
@ -93,8 +97,8 @@ const NewsDetail: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 封面图 */}
|
||||
{data.cover && (
|
||||
<div className="detail-cover" style={{ backgroundImage: `url(${data.cover})` }}>
|
||||
{data.coverImage && (
|
||||
<div className="detail-cover" style={{ backgroundImage: `url(${data.coverImage})` }}>
|
||||
<div className="cover-overlay"></div>
|
||||
</div>
|
||||
)}
|
||||
@ -102,63 +106,96 @@ const NewsDetail: React.FC = () => {
|
||||
{/* 详情内容 */}
|
||||
<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-layout" style={{ display: 'flex', gap: '24px' }}>
|
||||
{/* 主要内容 */}
|
||||
<div className="detail-main" style={{ flex: 1 }}>
|
||||
{/* 头部信息 */}
|
||||
<div className="detail-header">
|
||||
<Tag color={categoryColors[data.category]} className="category-tag">
|
||||
{categoryLabels[data.category] || data.category}
|
||||
</Tag>
|
||||
<Title level={1} className="detail-title">
|
||||
{data.title}
|
||||
</Title>
|
||||
<div className="detail-meta">
|
||||
<Space size="large" wrap>
|
||||
<span className="meta-item">
|
||||
<UserOutlined />
|
||||
<Text>作者:{data.author}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<Text>来源:{data.source}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<CalendarOutlined />
|
||||
<Text>发布时间:{dayjs(data.publishTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<EyeOutlined />
|
||||
<Text>{data.viewCount.toLocaleString()} 浏览</Text>
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<LikeOutlined />
|
||||
<Text>{data.likeCount.toLocaleString()} 点赞</Text>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 摘要 */}
|
||||
<div className="detail-summary">
|
||||
<Paragraph className="summary-text">{data.summary}</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 正文内容 - 使用 dangerouslySetInnerHTML 渲染 HTML */}
|
||||
<div
|
||||
className="detail-body"
|
||||
dangerouslySetInnerHTML={{ __html: data.content }}
|
||||
/>
|
||||
|
||||
{/* 标签 */}
|
||||
{tagArray.length > 0 && (
|
||||
<div className="detail-tags" style={{ marginTop: '24px' }}>
|
||||
<Space size="small" wrap>
|
||||
<Text type="secondary">标签:</Text>
|
||||
{tagArray.map((tag, index) => (
|
||||
<Tag key={`${tag}-${index}`}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 评论区 */}
|
||||
<div className="detail-comments">
|
||||
<CommentSection targetType="news" targetId={data.id} />
|
||||
</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>
|
||||
))}
|
||||
{/* 侧边栏 */}
|
||||
<div className="detail-sidebar" style={{ width: '280px' }}>
|
||||
{/* 操作按钮 */}
|
||||
<Card className="sidebar-card" style={{ marginBottom: 24 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<FavoriteButton
|
||||
targetType="news"
|
||||
targetId={data.id}
|
||||
size="large"
|
||||
showText
|
||||
/>
|
||||
<LikeButton
|
||||
targetType="news"
|
||||
targetId={data.id}
|
||||
size="large"
|
||||
showText
|
||||
initialCount={data.likeCount}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -166,4 +203,4 @@ const NewsDetail: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default NewsDetail
|
||||
export default NewsDetailPage
|
||||
|
||||
@ -8,54 +8,43 @@ 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 { getNewsList } from '@services/newsApi'
|
||||
import { getEventList } from '@services/eventApi'
|
||||
import type { NewsListItem, EventListItem, IPage, NewsCategory, EventStatus } from '@/types'
|
||||
import './index.css'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
// 资讯分类选项
|
||||
// 资讯分类选项(匹配后端 category 字段:news、activity、notice)
|
||||
const newsCategoryOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '展览', value: 'exhibition' },
|
||||
{ label: '活动', value: 'activity' },
|
||||
{ label: '政策', value: 'policy' },
|
||||
{ label: '研究', value: 'research' },
|
||||
{ label: '故事', value: 'story' },
|
||||
{ label: '新闻资讯', value: 'news' },
|
||||
{ label: '活动动态', value: 'activity' },
|
||||
{ label: '通知公告', value: 'notice' },
|
||||
]
|
||||
|
||||
// 活动类型选项
|
||||
const eventTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '展览', value: 'exhibition' },
|
||||
{ label: '工作坊', value: 'workshop' },
|
||||
{ label: '演出', value: 'performance' },
|
||||
{ label: '讲座', value: 'lecture' },
|
||||
{ label: '节日', value: 'festival' },
|
||||
]
|
||||
|
||||
// 活动状态选项
|
||||
// 活动状态选项(匹配后端 status 字段:upcoming、ongoing、finished、cancelled)
|
||||
const eventStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '即将开始', value: 'upcoming' },
|
||||
{ label: '进行中', value: 'ongoing' },
|
||||
{ label: '已结束', value: 'finished' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
]
|
||||
|
||||
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 [newsData, setNewsData] = useState<IPage<NewsListItem> | null>(null)
|
||||
const [eventsData, setEventsData] = useState<IPage<EventListItem> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 资讯筛选条件
|
||||
const [newsCategory, setNewsCategory] = useState('')
|
||||
const [newsCategory, setNewsCategory] = useState<NewsCategory | ''>('')
|
||||
const [newsPage, setNewsPage] = useState(1)
|
||||
const [newsPageSize, setNewsPageSize] = useState(12)
|
||||
|
||||
// 活动筛选条件
|
||||
const [eventType, setEventType] = useState('')
|
||||
const [eventStatus, setEventStatus] = useState('')
|
||||
const [eventStatus, setEventStatus] = useState<EventStatus | ''>('')
|
||||
const [eventPage, setEventPage] = useState(1)
|
||||
const [eventPageSize, setEventPageSize] = useState(12)
|
||||
|
||||
@ -64,9 +53,9 @@ const NewsPage: React.FC = () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getNewsList({
|
||||
category: (newsCategory || undefined) as any,
|
||||
page: newsPage,
|
||||
pageNum: newsPage,
|
||||
pageSize: newsPageSize,
|
||||
category: newsCategory || undefined,
|
||||
})
|
||||
setNewsData(result)
|
||||
} catch (error) {
|
||||
@ -81,10 +70,9 @@ const NewsPage: React.FC = () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getEventList({
|
||||
type: eventType || undefined,
|
||||
status: eventStatus || undefined,
|
||||
page: eventPage,
|
||||
pageNum: eventPage,
|
||||
pageSize: eventPageSize,
|
||||
status: eventStatus || undefined,
|
||||
})
|
||||
setEventsData(result)
|
||||
} catch (error) {
|
||||
@ -100,7 +88,7 @@ const NewsPage: React.FC = () => {
|
||||
} else {
|
||||
fetchEvents()
|
||||
}
|
||||
}, [activeTab, newsCategory, newsPage, newsPageSize, eventType, eventStatus, eventPage, eventPageSize])
|
||||
}, [activeTab, newsCategory, newsPage, newsPageSize, eventStatus, eventPage, eventPageSize])
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key)
|
||||
@ -165,10 +153,10 @@ const NewsPage: React.FC = () => {
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : newsData && newsData.data.length > 0 ? (
|
||||
) : newsData && newsData.records.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{newsData.data.map((item) => (
|
||||
{newsData.records.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<NewsCard item={item} />
|
||||
</Col>
|
||||
@ -199,13 +187,6 @@ const NewsPage: React.FC = () => {
|
||||
{/* 活动筛选 */}
|
||||
<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}
|
||||
@ -221,10 +202,10 @@ const NewsPage: React.FC = () => {
|
||||
<div className="loading-container">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : eventsData && eventsData.data.length > 0 ? (
|
||||
) : eventsData && eventsData.records.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[24, 24]}>
|
||||
{eventsData.data.map((item) => (
|
||||
{eventsData.records.map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<EventCard item={item} />
|
||||
</Col>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user