feat: 热点事件UI调整成轮播图
This commit is contained in:
@@ -145,7 +145,7 @@ export const fetchHotEvents = createAsyncThunk(
|
|||||||
try {
|
try {
|
||||||
return await fetchWithCache({
|
return await fetchWithCache({
|
||||||
cacheKey: CACHE_KEYS.HOT_EVENTS,
|
cacheKey: CACHE_KEYS.HOT_EVENTS,
|
||||||
fetchFn: () => eventService.getHotEvents({ days: 5, limit: 4 }),
|
fetchFn: () => eventService.getHotEvents({ days: 5, limit: 20 }),
|
||||||
getState,
|
getState,
|
||||||
stateKey: 'hotEvents',
|
stateKey: 'hotEvents',
|
||||||
forceRefresh
|
forceRefresh
|
||||||
|
|||||||
@@ -16,11 +16,76 @@
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Carousel */
|
||||||
|
.carousel-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-counter {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; /* 容器内部顶部 */
|
||||||
|
right: 48px; /* 避开右侧箭头 */
|
||||||
|
z-index: 100; /* 确保在卡片和箭头上方 */
|
||||||
|
background: rgba(24, 144, 255, 0.95);
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
pointer-events: none; /* 不阻挡鼠标事件 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-events-carousel {
|
||||||
|
padding: 0 40px; /* 增加左右padding为箭头留出空间 */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-events-carousel .carousel-item {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义箭头样式 */
|
||||||
|
.custom-carousel-arrow {
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
z-index: 10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-carousel-arrow:hover {
|
||||||
|
background: rgba(255, 255, 255, 1) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-carousel-arrow:hover .anticon {
|
||||||
|
color: #096dd9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 箭头位置 */
|
||||||
|
.hot-events-carousel .slick-prev.custom-carousel-arrow {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-events-carousel .slick-next.custom-carousel-arrow {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用状态 */
|
||||||
|
.custom-carousel-arrow.slick-disabled {
|
||||||
|
opacity: 0.3 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Card */
|
/* Card */
|
||||||
.hot-event-card {
|
.hot-event-card {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hot-event-card:hover {
|
.hot-event-card:hover {
|
||||||
@@ -28,11 +93,16 @@
|
|||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cover image */
|
/* Card body padding */
|
||||||
|
.hot-event-card .ant-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover image - 高度减半 */
|
||||||
.event-cover {
|
.event-cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 160px;
|
height: 80px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,28 +124,53 @@
|
|||||||
|
|
||||||
/* Card content */
|
/* Card content */
|
||||||
.event-header {
|
.event-header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-header .ant-tag {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-title {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #000;
|
color: #000;
|
||||||
flex: 1;
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标题文字 - inline显示,可以换行 */
|
||||||
|
.event-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签紧跟标题后面 */
|
||||||
|
.event-tag {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tag .ant-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
transform: scale(0.9);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情描述 - 三行省略 */
|
||||||
.event-description {
|
.event-description {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #595959;
|
color: #595959;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-height: 4.5em;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-footer {
|
.event-footer {
|
||||||
@@ -83,6 +178,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #8c8c8c;
|
color: #8c8c8c;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creator {
|
.creator {
|
||||||
@@ -92,6 +188,19 @@
|
|||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 时间样式 - 年月日高亮 */
|
||||||
.time {
|
.time {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-date {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-hour {
|
||||||
|
color: #8c8c8c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
// src/views/Community/components/HotEvents.js
|
// src/views/Community/components/HotEvents.js
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, Row, Col, Badge, Tag, Empty } from 'antd';
|
import { Card, Badge, Tag, Empty, Carousel, Tooltip } from 'antd';
|
||||||
import { ArrowUpOutlined, ArrowDownOutlined, FireOutlined } from '@ant-design/icons';
|
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import './HotEvents.css';
|
import './HotEvents.css';
|
||||||
import defaultEventImage from '../../../assets/img/default-event.jpg'
|
import defaultEventImage from '../../../assets/img/default-event.jpg'
|
||||||
const HotEvents = ({ events }) => {
|
|
||||||
|
// 自定义箭头组件
|
||||||
|
const CustomArrow = ({ className, style, onClick, direction }) => {
|
||||||
|
const Icon = direction === 'left' ? LeftOutlined : RightOutlined;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${className} custom-carousel-arrow`}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Icon style={{ fontSize: '20px', color: '#1890ff' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const HotEvents = ({ events, onPageChange }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
|
||||||
const renderPriceChange = (value) => {
|
const renderPriceChange = (value) => {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
@@ -39,12 +60,60 @@ const HotEvents = ({ events }) => {
|
|||||||
navigate(`/event-detail/${eventId}`);
|
navigate(`/event-detail/${eventId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = Math.ceil((events?.length || 0) / 4);
|
||||||
|
|
||||||
|
// Carousel 配置
|
||||||
|
const carouselSettings = {
|
||||||
|
dots: false, // 隐藏圆点导航
|
||||||
|
infinite: true, // 始终启用无限循环,确保箭头显示
|
||||||
|
speed: 500,
|
||||||
|
slidesToShow: 4,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
arrows: true, // 保留左右箭头
|
||||||
|
prevArrow: <CustomArrow direction="left" />,
|
||||||
|
nextArrow: <CustomArrow direction="right" />,
|
||||||
|
autoplay: false,
|
||||||
|
beforeChange: (_current, next) => {
|
||||||
|
// 计算实际页码(考虑无限循环)
|
||||||
|
const actualPage = next % totalPages;
|
||||||
|
setCurrentSlide(actualPage);
|
||||||
|
// 通知父组件页码变化
|
||||||
|
if (onPageChange) {
|
||||||
|
onPageChange(actualPage + 1, totalPages);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: [
|
||||||
|
{
|
||||||
|
breakpoint: 1200,
|
||||||
|
settings: {
|
||||||
|
slidesToShow: 3,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
breakpoint: 992,
|
||||||
|
settings: {
|
||||||
|
slidesToShow: 2,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
breakpoint: 576,
|
||||||
|
settings: {
|
||||||
|
slidesToShow: 1,
|
||||||
|
slidesToScroll: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hot-events-section">
|
<div className="hot-events-section">
|
||||||
{events && events.length > 0 ? (
|
{events && events.length > 0 ? (
|
||||||
<Row gutter={[16, 16]}>
|
<Carousel {...carouselSettings} className="hot-events-carousel">
|
||||||
{events.map((event, index) => (
|
{events.map((event, index) => (
|
||||||
<Col lg={6} md={12} sm={24} key={event.id}>
|
<div key={event.id} className="carousel-item">
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
className="hot-event-card"
|
className="hot-event-card"
|
||||||
@@ -69,33 +138,36 @@ const HotEvents = ({ events }) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card.Meta
|
{/* Custom layout without Card.Meta */}
|
||||||
title={
|
<div className="event-header">
|
||||||
<div className="event-header">
|
<Tooltip title={event.title}>
|
||||||
{renderPriceChange(event.related_avg_chg)}
|
<span className="event-title">
|
||||||
<span className="event-title">
|
{event.title}
|
||||||
{event.title}
|
</span>
|
||||||
</span>
|
</Tooltip>
|
||||||
</div>
|
<span className="event-tag">
|
||||||
}
|
{renderPriceChange(event.related_avg_chg)}
|
||||||
description={
|
</span>
|
||||||
<>
|
</div>
|
||||||
<p className="event-description">
|
|
||||||
{event.description && event.description.length > 80
|
<Tooltip title={event.description}>
|
||||||
? `${event.description.substring(0, 80)}...`
|
<div className="event-description">
|
||||||
: event.description}
|
{event.description}
|
||||||
</p>
|
</div>
|
||||||
<div className="event-footer">
|
</Tooltip>
|
||||||
<span className="creator">{event.creator?.username || 'Anonymous'}</span>
|
|
||||||
<span className="time">{moment(event.created_at).format('MM-DD HH:mm')}</span>
|
<div className="event-footer">
|
||||||
</div>
|
<span className="creator">{event.creator?.username || 'Anonymous'}</span>
|
||||||
</>
|
<span className="time">
|
||||||
}
|
<span className="time-date">{moment(event.created_at).format('YYYY-MM-DD')}</span>
|
||||||
/>
|
{' '}
|
||||||
|
<span className="time-hour">{moment(event.created_at).format('HH:mm')}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Carousel>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<Empty description="暂无热点信息" />
|
<Empty description="暂无热点信息" />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// src/views/Community/components/HotEventsSection.js
|
// src/views/Community/components/HotEventsSection.js
|
||||||
// 热点事件区域组件
|
// 热点事件区域组件
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardBody,
|
CardBody,
|
||||||
Heading,
|
Heading,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
useColorModeValue
|
useColorModeValue
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import HotEvents from './HotEvents';
|
import HotEvents from './HotEvents';
|
||||||
@@ -17,6 +19,14 @@ import HotEvents from './HotEvents';
|
|||||||
*/
|
*/
|
||||||
const HotEventsSection = ({ events }) => {
|
const HotEventsSection = ({ events }) => {
|
||||||
const cardBg = useColorModeValue('white', 'gray.800');
|
const cardBg = useColorModeValue('white', 'gray.800');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handlePageChange = (page, total) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setTotalPages(total);
|
||||||
|
};
|
||||||
|
|
||||||
// 如果没有热点事件,不渲染组件
|
// 如果没有热点事件,不渲染组件
|
||||||
if (!events || events.length === 0) {
|
if (!events || events.length === 0) {
|
||||||
@@ -25,12 +35,27 @@ const HotEventsSection = ({ events }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card mt={0} bg={cardBg}>
|
<Card mt={0} bg={cardBg}>
|
||||||
<CardHeader pb={0}>
|
<CardHeader pb={0} display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||||
<Heading size="md">🔥 热点事件</Heading>
|
<Box>
|
||||||
<p className="section-subtitle" style={{paddingTop: '8px'}}>展示最近5天内涨幅最高的事件,助您把握市场热点</p>
|
<Heading size="md">🔥 热点事件</Heading>
|
||||||
|
<p className="section-subtitle" style={{paddingTop: '8px'}}>展示最近5天内涨幅最高的事件,助您把握市场热点</p>
|
||||||
|
</Box>
|
||||||
|
{/* 页码指示器 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Badge
|
||||||
|
colorScheme="blue"
|
||||||
|
fontSize="sm"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
borderRadius="full"
|
||||||
|
ml={4}
|
||||||
|
>
|
||||||
|
{currentPage} / {totalPages}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody py={0} px={4}>
|
<CardBody py={0} px={4}>
|
||||||
<HotEvents events={events} />
|
<HotEvents events={events} onPageChange={handlePageChange} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user