community增加事件详情
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
// src/views/Community/components/HeroPanel.js
|
// src/views/Community/components/HeroPanel.js
|
||||||
// 顶部说明面板组件:事件中心 + 沪深指数K线图 + 热门概念3D动画
|
// 顶部说明面板组件:事件中心标题 + 投资日历
|
||||||
// 交易时间内自动更新指数行情(每分钟一次)
|
// 简化版本:移除了指数K线和热门概念,由右侧边栏提供
|
||||||
|
|
||||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
@@ -24,15 +24,17 @@ import {
|
|||||||
ModalBody,
|
ModalBody,
|
||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
IconButton,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { AlertCircle, Clock, TrendingUp, Info, RefreshCw } from 'lucide-react';
|
import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import dayjs from 'dayjs';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
import { getConceptHtmlUrl } from '@utils/textUtils';
|
|
||||||
import { useIndexQuote } from '@hooks/useIndexQuote';
|
|
||||||
import conceptStaticService from '@services/conceptStaticService';
|
|
||||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||||
|
import InvestmentCalendar from '@components/InvestmentCalendar';
|
||||||
|
|
||||||
// 定义动画
|
// 定义动画
|
||||||
const animations = `
|
const animations = `
|
||||||
@@ -44,10 +46,6 @@ const animations = `
|
|||||||
0% { background-position: -200% 0; }
|
0% { background-position: -200% 0; }
|
||||||
100% { background-position: 200% 0; }
|
100% { background-position: 200% 0; }
|
||||||
}
|
}
|
||||||
@keyframes floatSlow {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-10px); }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 注入样式
|
// 注入样式
|
||||||
@@ -61,40 +59,6 @@ if (typeof document !== 'undefined') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指数行情数据(日线数据)
|
|
||||||
*/
|
|
||||||
const fetchIndexKline = async (indexCode) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${getApiBase()}/api/index/${indexCode}/kline?type=daily`);
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('HeroPanel', 'fetchIndexKline error', { indexCode, error: error.message });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取热门概念数据(使用静态文件)
|
|
||||||
*/
|
|
||||||
const fetchPopularConcepts = async () => {
|
|
||||||
try {
|
|
||||||
const result = await conceptStaticService.fetchPopularConcepts();
|
|
||||||
if (result.success && result.data?.results?.length > 0) {
|
|
||||||
return result.data.results.map(item => ({
|
|
||||||
name: item.concept,
|
|
||||||
change_pct: item.price_info?.avg_change_pct || 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('HeroPanel', 'fetchPopularConcepts error', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断当前是否在交易时间内
|
* 判断当前是否在交易时间内
|
||||||
*/
|
*/
|
||||||
@@ -105,501 +69,294 @@ const isInTradingTime = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 精美K线指数卡片 - 类似 KLineChartModal 风格
|
* 获取月度事件统计
|
||||||
* 交易时间内自动更新实时行情(每分钟一次)
|
|
||||||
*/
|
*/
|
||||||
const CompactIndexCard = ({ indexCode, indexName }) => {
|
const fetchEventCounts = async (year, month) => {
|
||||||
const [chartData, setChartData] = useState(null);
|
try {
|
||||||
const [loading, setLoading] = useState(true);
|
const response = await fetch(`${getApiBase()}/api/events/calendar/counts?year=${year}&month=${month}`);
|
||||||
const [latestData, setLatestData] = useState(null);
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
const upColor = '#ef5350'; // 涨 - 红色
|
return data.success ? data.data : [];
|
||||||
const downColor = '#26a69a'; // 跌 - 绿色
|
} catch (error) {
|
||||||
|
logger.error('HeroPanel', 'fetchEventCounts error', { year, month, error: error.message });
|
||||||
// 使用实时行情 Hook - 交易时间内每分钟自动更新
|
return [];
|
||||||
const { quote, isTrading, refresh: refreshQuote } = useIndexQuote(indexCode, {
|
|
||||||
refreshInterval: 60000, // 1分钟
|
|
||||||
autoRefresh: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载日K线图数据
|
|
||||||
const loadChartData = useCallback(async () => {
|
|
||||||
const data = await fetchIndexKline(indexCode);
|
|
||||||
if (data?.data?.length > 0) {
|
|
||||||
const recentData = data.data.slice(-60); // 最近60天
|
|
||||||
setChartData({
|
|
||||||
dates: recentData.map(item => item.time),
|
|
||||||
klineData: recentData.map(item => [item.open, item.close, item.low, item.high]),
|
|
||||||
volumes: recentData.map(item => item.volume || 0),
|
|
||||||
rawData: recentData
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果没有实时行情,使用日线数据的最新值
|
|
||||||
if (!quote) {
|
|
||||||
const latest = data.data[data.data.length - 1];
|
|
||||||
const prevClose = latest.prev_close || data.data[data.data.length - 2]?.close || latest.open;
|
|
||||||
const changeAmount = latest.close - prevClose;
|
|
||||||
const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0;
|
|
||||||
|
|
||||||
setLatestData({
|
|
||||||
close: latest.close,
|
|
||||||
open: latest.open,
|
|
||||||
high: latest.high,
|
|
||||||
low: latest.low,
|
|
||||||
changeAmount: changeAmount,
|
|
||||||
changePct: changePct,
|
|
||||||
isPositive: changeAmount >= 0
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}, [indexCode, quote]);
|
|
||||||
|
|
||||||
// 初始加载日K数据
|
|
||||||
useEffect(() => {
|
|
||||||
loadChartData();
|
|
||||||
}, [loadChartData]);
|
|
||||||
|
|
||||||
// 当实时行情更新时,更新 latestData
|
|
||||||
useEffect(() => {
|
|
||||||
if (quote) {
|
|
||||||
setLatestData({
|
|
||||||
close: quote.price,
|
|
||||||
open: quote.open,
|
|
||||||
high: quote.high,
|
|
||||||
low: quote.low,
|
|
||||||
changeAmount: quote.change,
|
|
||||||
changePct: quote.change_pct,
|
|
||||||
isPositive: quote.change >= 0,
|
|
||||||
updateTime: quote.update_time,
|
|
||||||
isRealtime: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [quote]);
|
|
||||||
|
|
||||||
const chartOption = useMemo(() => {
|
|
||||||
if (!chartData) return {};
|
|
||||||
return {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
grid: [
|
|
||||||
{ left: 0, right: 0, top: 8, bottom: 28, containLabel: false },
|
|
||||||
{ left: 0, right: 0, top: '75%', bottom: 4, containLabel: false }
|
|
||||||
],
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'cross',
|
|
||||||
crossStyle: { color: 'rgba(255, 215, 0, 0.6)', width: 1 },
|
|
||||||
lineStyle: { color: 'rgba(255, 215, 0, 0.4)', width: 1, type: 'dashed' }
|
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(15, 15, 25, 0.98)',
|
|
||||||
borderColor: 'rgba(255, 215, 0, 0.5)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: [12, 16],
|
|
||||||
textStyle: { color: '#e0e0e0', fontSize: 12 },
|
|
||||||
extraCssText: 'box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);',
|
|
||||||
formatter: (params) => {
|
|
||||||
const idx = params[0]?.dataIndex;
|
|
||||||
if (idx === undefined) return '';
|
|
||||||
|
|
||||||
const raw = chartData.rawData[idx];
|
|
||||||
if (!raw) return '';
|
|
||||||
|
|
||||||
// 安全格式化数字
|
|
||||||
const safeFixed = (val, digits = 2) => (val != null && !isNaN(val)) ? val.toFixed(digits) : '-';
|
|
||||||
|
|
||||||
// 计算涨跌
|
|
||||||
const prevClose = raw.prev_close || (idx > 0 ? chartData.rawData[idx - 1]?.close : raw.open) || raw.open;
|
|
||||||
const changeAmount = (raw.close != null && prevClose != null) ? (raw.close - prevClose) : 0;
|
|
||||||
const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0;
|
|
||||||
const isUp = changeAmount >= 0;
|
|
||||||
const color = isUp ? '#ef5350' : '#26a69a';
|
|
||||||
const sign = isUp ? '+' : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div style="min-width: 180px;">
|
|
||||||
<div style="font-weight: bold; color: #FFD700; margin-bottom: 10px; font-size: 13px; border-bottom: 1px solid rgba(255,215,0,0.2); padding-bottom: 8px;">
|
|
||||||
📅 ${raw.time || '-'}
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; font-size: 12px;">
|
|
||||||
<span style="color: #999;">开盘</span>
|
|
||||||
<span style="text-align: right; font-family: monospace;">${safeFixed(raw.open)}</span>
|
|
||||||
<span style="color: #999;">收盘</span>
|
|
||||||
<span style="text-align: right; font-weight: bold; color: ${color}; font-family: monospace;">${safeFixed(raw.close)}</span>
|
|
||||||
<span style="color: #999;">最高</span>
|
|
||||||
<span style="text-align: right; color: #ef5350; font-family: monospace;">${safeFixed(raw.high)}</span>
|
|
||||||
<span style="color: #999;">最低</span>
|
|
||||||
<span style="text-align: right; color: #26a69a; font-family: monospace;">${safeFixed(raw.low)}</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span style="color: #999; font-size: 11px;">涨跌幅</span>
|
|
||||||
<span style="color: ${color}; font-weight: bold; font-size: 14px; font-family: monospace;">
|
|
||||||
${sign}${safeFixed(changeAmount)} (${sign}${safeFixed(changePct)}%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
data: chartData.dates,
|
|
||||||
gridIndex: 0,
|
|
||||||
show: false,
|
|
||||||
boundaryGap: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
data: chartData.dates,
|
|
||||||
gridIndex: 1,
|
|
||||||
show: false,
|
|
||||||
boundaryGap: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
yAxis: [
|
|
||||||
{
|
|
||||||
type: 'value',
|
|
||||||
gridIndex: 0,
|
|
||||||
show: false,
|
|
||||||
scale: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'value',
|
|
||||||
gridIndex: 1,
|
|
||||||
show: false,
|
|
||||||
scale: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dataZoom: [{
|
|
||||||
type: 'inside',
|
|
||||||
xAxisIndex: [0, 1],
|
|
||||||
start: 50,
|
|
||||||
end: 100,
|
|
||||||
zoomOnMouseWheel: true,
|
|
||||||
moveOnMouseMove: true
|
|
||||||
}],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'K线',
|
|
||||||
type: 'candlestick',
|
|
||||||
data: chartData.klineData,
|
|
||||||
xAxisIndex: 0,
|
|
||||||
yAxisIndex: 0,
|
|
||||||
itemStyle: {
|
|
||||||
color: upColor,
|
|
||||||
color0: downColor,
|
|
||||||
borderColor: upColor,
|
|
||||||
borderColor0: downColor,
|
|
||||||
borderWidth: 1
|
|
||||||
},
|
|
||||||
barWidth: '65%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '成交量',
|
|
||||||
type: 'bar',
|
|
||||||
data: chartData.volumes,
|
|
||||||
xAxisIndex: 1,
|
|
||||||
yAxisIndex: 1,
|
|
||||||
itemStyle: {
|
|
||||||
color: (params) => {
|
|
||||||
const idx = params.dataIndex;
|
|
||||||
const raw = chartData.rawData[idx];
|
|
||||||
return raw && raw.close >= raw.open ? 'rgba(239,83,80,0.5)' : 'rgba(38,166,154,0.5)';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
barWidth: '65%'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}, [chartData, upColor, downColor]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Center h="210px">
|
|
||||||
<VStack spacing={2}>
|
|
||||||
<Spinner size="sm" color="gold" thickness="2px" />
|
|
||||||
<Text fontSize="10px" color="whiteAlpha.500">加载{indexName}...</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" h="210px">
|
|
||||||
{/* 顶部:指数名称和数据 */}
|
|
||||||
<Flex justify="space-between" align="center" mb={1}>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Box
|
|
||||||
w="3px"
|
|
||||||
h="14px"
|
|
||||||
borderRadius="full"
|
|
||||||
bg={latestData?.isPositive ? '#ef5350' : '#26a69a'}
|
|
||||||
/>
|
|
||||||
<Text fontSize="sm" color="whiteAlpha.800" fontWeight="semibold">
|
|
||||||
{indexName}
|
|
||||||
</Text>
|
|
||||||
{/* 实时状态指示 */}
|
|
||||||
{isTrading && latestData?.isRealtime && (
|
|
||||||
<Tooltip label="实时行情,每分钟更新" placement="top">
|
|
||||||
<HStack
|
|
||||||
spacing={1}
|
|
||||||
px={1.5}
|
|
||||||
py={0.5}
|
|
||||||
bg="rgba(0,218,60,0.1)"
|
|
||||||
borderRadius="full"
|
|
||||||
border="1px solid rgba(0,218,60,0.3)"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
w="5px"
|
|
||||||
h="5px"
|
|
||||||
borderRadius="full"
|
|
||||||
bg="#00da3c"
|
|
||||||
animation="pulse 1.5s infinite"
|
|
||||||
/>
|
|
||||||
<Text fontSize="9px" color="#00da3c" fontWeight="bold">
|
|
||||||
实时
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={3}>
|
|
||||||
<Text fontSize="lg" fontWeight="bold" color="white" fontFamily="monospace">
|
|
||||||
{latestData?.close?.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
<Box
|
|
||||||
px={2}
|
|
||||||
py={0.5}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={latestData?.isPositive ? 'rgba(239,83,80,0.15)' : 'rgba(38,166,154,0.15)'}
|
|
||||||
border="1px solid"
|
|
||||||
borderColor={latestData?.isPositive ? 'rgba(239,83,80,0.3)' : 'rgba(38,166,154,0.3)'}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
fontSize="sm"
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="monospace"
|
|
||||||
color={latestData?.isPositive ? '#ef5350' : '#26a69a'}
|
|
||||||
>
|
|
||||||
{latestData?.isPositive ? '▲' : '▼'} {latestData?.isPositive ? '+' : ''}{latestData?.changePct?.toFixed(2)}%
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* K线图区域 */}
|
|
||||||
<Box flex="1" position="relative">
|
|
||||||
<ReactECharts
|
|
||||||
option={chartOption}
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
opts={{ renderer: 'canvas' }}
|
|
||||||
/>
|
|
||||||
{/* 底部提示 - 显示更新时间 */}
|
|
||||||
<HStack
|
|
||||||
position="absolute"
|
|
||||||
bottom={0}
|
|
||||||
right={1}
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
{latestData?.updateTime && (
|
|
||||||
<Text fontSize="9px" color="whiteAlpha.400">
|
|
||||||
{latestData.updateTime}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text fontSize="9px" color="whiteAlpha.300">
|
|
||||||
滚轮缩放 · 拖动查看
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流动式热门概念组件 - HeroUI 风格
|
* 紧凑型投资日历组件 - 深色主题风格
|
||||||
* 特点:
|
* 特点:
|
||||||
* 1. 三行横向滚动,每行方向不同
|
* 1. 紧凑的月历视图
|
||||||
* 2. 卡片式设计,带渐变边框
|
* 2. 事件数量用圆点颜色深浅表示
|
||||||
* 3. 悬停时暂停滚动,放大效果
|
* 3. 悬停显示当日事件数
|
||||||
* 4. 流光动画效果
|
* 4. 点击打开完整日历弹窗
|
||||||
*/
|
*/
|
||||||
const FlowingConcepts = () => {
|
const CompactCalendar = () => {
|
||||||
const [concepts, setConcepts] = useState([]);
|
const [currentMonth, setCurrentMonth] = useState(dayjs());
|
||||||
|
const [eventCounts, setEventCounts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [hoveredIdx, setHoveredIdx] = useState(null);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
|
||||||
|
// 加载月度事件统计
|
||||||
|
const loadEventCounts = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await fetchEventCounts(currentMonth.year(), currentMonth.month() + 1);
|
||||||
|
setEventCounts(data);
|
||||||
|
setLoading(false);
|
||||||
|
}, [currentMonth]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
loadEventCounts();
|
||||||
const data = await fetchPopularConcepts();
|
}, [loadEventCounts]);
|
||||||
setConcepts(data.slice(0, 30)); // 取30个概念
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getColor = (pct) => {
|
// 获取事件数量映射
|
||||||
if (pct > 5) return { bg: 'rgba(255,23,68,0.15)', border: '#ff1744', text: '#ff1744', glow: 'rgba(255,23,68,0.4)' };
|
const getEventCountForDate = (dateStr) => {
|
||||||
if (pct > 2) return { bg: 'rgba(255,82,82,0.12)', border: '#ff5252', text: '#ff5252', glow: 'rgba(255,82,82,0.3)' };
|
const found = eventCounts.find(item => item.date === dateStr);
|
||||||
if (pct > 0) return { bg: 'rgba(255,138,128,0.1)', border: '#ff8a80', text: '#ff8a80', glow: 'rgba(255,138,128,0.25)' };
|
return found ? found.count : 0;
|
||||||
if (pct === 0) return { bg: 'rgba(255,215,0,0.1)', border: '#FFD700', text: '#FFD700', glow: 'rgba(255,215,0,0.25)' };
|
|
||||||
if (pct > -2) return { bg: 'rgba(105,240,174,0.1)', border: '#69f0ae', text: '#69f0ae', glow: 'rgba(105,240,174,0.25)' };
|
|
||||||
if (pct > -5) return { bg: 'rgba(0,230,118,0.12)', border: '#00e676', text: '#00e676', glow: 'rgba(0,230,118,0.3)' };
|
|
||||||
return { bg: 'rgba(0,200,83,0.15)', border: '#00c853', text: '#00c853', glow: 'rgba(0,200,83,0.4)' };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (name) => {
|
// 根据事件数量获取颜色
|
||||||
window.open(getConceptHtmlUrl(name), '_blank');
|
const getEventColor = (count) => {
|
||||||
|
if (count === 0) return 'transparent';
|
||||||
|
if (count >= 15) return 'rgba(239, 68, 68, 0.9)'; // 深红
|
||||||
|
if (count >= 10) return 'rgba(249, 115, 22, 0.85)'; // 橙红
|
||||||
|
if (count >= 5) return 'rgba(234, 179, 8, 0.8)'; // 金黄
|
||||||
|
if (count >= 3) return 'rgba(34, 197, 94, 0.75)'; // 绿色
|
||||||
|
return 'rgba(59, 130, 246, 0.7)'; // 蓝色
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
// 生成日历网格
|
||||||
return (
|
const generateCalendarDays = () => {
|
||||||
<Center h="100%">
|
const startOfMonth = currentMonth.startOf('month');
|
||||||
<VStack spacing={2}>
|
const endOfMonth = currentMonth.endOf('month');
|
||||||
<Spinner size="md" color="gold" thickness="3px" />
|
const startDay = startOfMonth.day(); // 0=周日
|
||||||
<Text fontSize="xs" color="whiteAlpha.500">加载热门概念...</Text>
|
const daysInMonth = endOfMonth.date();
|
||||||
</VStack>
|
|
||||||
</Center>
|
const days = [];
|
||||||
);
|
const today = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 填充月初空白
|
||||||
|
for (let i = 0; i < startDay; i++) {
|
||||||
|
days.push({ date: null, day: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将概念分成三行
|
// 填充日期
|
||||||
const row1 = concepts.slice(0, 10);
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
const row2 = concepts.slice(10, 20);
|
const date = currentMonth.date(day).format('YYYY-MM-DD');
|
||||||
const row3 = concepts.slice(20, 30);
|
const count = getEventCountForDate(date);
|
||||||
|
days.push({
|
||||||
|
date,
|
||||||
|
day,
|
||||||
|
count,
|
||||||
|
isToday: date === today,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染单个概念卡片
|
return days;
|
||||||
const renderConceptCard = (concept, globalIdx, uniqueIdx) => {
|
};
|
||||||
const colors = getColor(concept.change_pct);
|
|
||||||
const isActive = hoveredIdx === globalIdx;
|
const days = generateCalendarDays();
|
||||||
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Box
|
<Box
|
||||||
key={`${globalIdx}-${uniqueIdx}`}
|
bg="rgba(0,0,0,0.3)"
|
||||||
flexShrink={0}
|
backdropFilter={GLASS_BLUR.sm}
|
||||||
px={3}
|
|
||||||
py={2}
|
|
||||||
mx={2}
|
|
||||||
bg={isActive ? colors.bg : 'rgba(255,255,255,0.03)'}
|
|
||||||
border="1px solid"
|
|
||||||
borderColor={isActive ? colors.border : 'rgba(255,255,255,0.08)'}
|
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
cursor="pointer"
|
border="1px solid rgba(255,215,0,0.2)"
|
||||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
p={3}
|
||||||
transform={isActive ? 'scale(1.1) translateY(-4px)' : 'scale(1)'}
|
minW="280px"
|
||||||
boxShadow={isActive
|
|
||||||
? `0 8px 32px ${colors.glow}, 0 0 0 1px ${colors.border}40, inset 0 1px 0 rgba(255,255,255,0.1)`
|
|
||||||
: '0 2px 8px rgba(0,0,0,0.2)'
|
|
||||||
}
|
|
||||||
onClick={() => handleClick(concept.name)}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setHoveredIdx(globalIdx);
|
|
||||||
setIsPaused(true);
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoveredIdx(null);
|
|
||||||
setIsPaused(false);
|
|
||||||
}}
|
|
||||||
position="relative"
|
position="relative"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
_before={isActive ? {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: '-100%',
|
|
||||||
width: '200%',
|
|
||||||
height: '100%',
|
|
||||||
background: `linear-gradient(90deg, transparent, ${colors.glow}, transparent)`,
|
|
||||||
animation: 'shimmer 1.5s infinite',
|
|
||||||
} : {}}
|
|
||||||
>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Text
|
|
||||||
fontSize="sm"
|
|
||||||
fontWeight={isActive ? 'bold' : 'semibold'}
|
|
||||||
color={isActive ? colors.text : 'whiteAlpha.900'}
|
|
||||||
whiteSpace="nowrap"
|
|
||||||
textShadow={isActive ? `0 0 10px ${colors.glow}` : 'none'}
|
|
||||||
>
|
|
||||||
{concept.name}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="bold"
|
|
||||||
color={colors.text}
|
|
||||||
whiteSpace="nowrap"
|
|
||||||
>
|
|
||||||
{concept.change_pct > 0 ? '+' : ''}{concept.change_pct?.toFixed(2) ?? '-'}%
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染滚动行
|
|
||||||
const renderScrollRow = (items, direction, startIdx, duration) => {
|
|
||||||
const animationName = direction === 'left' ? 'scrollLeft' : 'scrollRight';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
|
||||||
py={1}
|
|
||||||
_before={{
|
_before={{
|
||||||
content: '""',
|
content: '""',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '60px',
|
|
||||||
background: 'linear-gradient(90deg, rgba(10,10,20,1) 0%, transparent 100%)',
|
|
||||||
zIndex: 2,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
_after={{
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
height: '1px',
|
||||||
bottom: 0,
|
background: 'linear-gradient(90deg, transparent, rgba(255,215,0,0.4), transparent)',
|
||||||
width: '60px',
|
|
||||||
background: 'linear-gradient(90deg, transparent 0%, rgba(10,10,20,1) 100%)',
|
|
||||||
zIndex: 2,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部:月份切换 */}
|
||||||
|
<Flex justify="space-between" align="center" mb={2}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box
|
||||||
|
p={1.5}
|
||||||
|
bg="rgba(255,215,0,0.15)"
|
||||||
|
borderRadius="lg"
|
||||||
|
border="1px solid rgba(255,215,0,0.25)"
|
||||||
|
>
|
||||||
|
<Icon as={Calendar} color="gold" boxSize={4} />
|
||||||
|
</Box>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color="gold">
|
||||||
|
投资日历
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ChevronLeft size={14} />}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
color="whiteAlpha.700"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
|
onClick={() => setCurrentMonth(prev => prev.subtract(1, 'month'))}
|
||||||
|
aria-label="上个月"
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" minW="70px" textAlign="center">
|
||||||
|
{currentMonth.format('YYYY年M月')}
|
||||||
|
</Text>
|
||||||
|
<IconButton
|
||||||
|
icon={<ChevronRight size={14} />}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
color="whiteAlpha.700"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
|
onClick={() => setCurrentMonth(prev => prev.add(1, 'month'))}
|
||||||
|
aria-label="下个月"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 星期头 */}
|
||||||
|
<Grid templateColumns="repeat(7, 1fr)" gap={0.5} mb={1}>
|
||||||
|
{weekDays.map((day, idx) => (
|
||||||
|
<GridItem key={day}>
|
||||||
|
<Text
|
||||||
|
fontSize="10px"
|
||||||
|
color={idx === 0 || idx === 6 ? 'orange.300' : 'whiteAlpha.500'}
|
||||||
|
textAlign="center"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</Text>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 日期网格 */}
|
||||||
|
{loading ? (
|
||||||
|
<Center h="140px">
|
||||||
|
<Spinner size="sm" color="gold" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Grid templateColumns="repeat(7, 1fr)" gap={0.5}>
|
||||||
|
{days.map((item, idx) => (
|
||||||
|
<GridItem key={idx}>
|
||||||
|
{item.day ? (
|
||||||
|
<Tooltip
|
||||||
|
label={item.count > 0 ? `${item.count}个事件` : '无事件'}
|
||||||
|
placement="top"
|
||||||
|
hasArrow
|
||||||
|
bg="gray.800"
|
||||||
|
color="white"
|
||||||
|
fontSize="xs"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
animation={`${animationName} ${duration}s linear infinite`}
|
direction="column"
|
||||||
sx={{
|
align="center"
|
||||||
animationPlayState: isPaused ? 'paused' : 'running',
|
justify="center"
|
||||||
[`@keyframes ${animationName}`]: direction === 'left' ? {
|
h="28px"
|
||||||
'0%': { transform: 'translateX(0)' },
|
borderRadius="md"
|
||||||
'100%': { transform: 'translateX(-50%)' },
|
cursor="pointer"
|
||||||
} : {
|
position="relative"
|
||||||
'0%': { transform: 'translateX(-50%)' },
|
bg={item.isToday ? 'rgba(255,215,0,0.2)' : 'transparent'}
|
||||||
'100%': { transform: 'translateX(0)' },
|
border={item.isToday ? '1px solid rgba(255,215,0,0.5)' : '1px solid transparent'}
|
||||||
},
|
_hover={{
|
||||||
|
bg: 'rgba(255,255,255,0.1)',
|
||||||
|
borderColor: 'rgba(255,255,255,0.2)',
|
||||||
}}
|
}}
|
||||||
|
transition="all 0.15s"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
>
|
>
|
||||||
{/* 复制两份实现无缝滚动 */}
|
<Text
|
||||||
{[...items, ...items].map((concept, idx) =>
|
fontSize="11px"
|
||||||
renderConceptCard(concept, startIdx + (idx % items.length), idx)
|
color={item.isToday ? 'gold' : 'whiteAlpha.800'}
|
||||||
|
fontWeight={item.isToday ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
{item.day}
|
||||||
|
</Text>
|
||||||
|
{/* 事件指示点 */}
|
||||||
|
{item.count > 0 && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom="2px"
|
||||||
|
w="4px"
|
||||||
|
h="4px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg={getEventColor(item.count)}
|
||||||
|
boxShadow={`0 0 4px ${getEventColor(item.count)}`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Tooltip>
|
||||||
);
|
) : (
|
||||||
};
|
<Box h="28px" />
|
||||||
|
)}
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
{/* 底部:查看更多 */}
|
||||||
<Box h="100%" w="100%" py={2}>
|
<Flex justify="center" mt={2}>
|
||||||
<VStack spacing={2} h="100%" justify="center">
|
<Text
|
||||||
{renderScrollRow(row1, 'left', 0, 35)}
|
fontSize="10px"
|
||||||
{renderScrollRow(row2, 'right', 10, 40)}
|
color="whiteAlpha.500"
|
||||||
{renderScrollRow(row3, 'left', 20, 32)}
|
cursor="pointer"
|
||||||
</VStack>
|
_hover={{ color: 'gold' }}
|
||||||
|
transition="color 0.2s"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
点击查看详细日历 →
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 完整日历弹窗 */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
size="6xl"
|
||||||
|
>
|
||||||
|
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
||||||
|
<ModalContent
|
||||||
|
maxW="1200px"
|
||||||
|
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
|
||||||
|
border="1px solid rgba(255,215,0,0.2)"
|
||||||
|
borderRadius="2xl"
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
borderBottom="1px solid rgba(255,215,0,0.2)"
|
||||||
|
color="white"
|
||||||
|
>
|
||||||
|
<HStack spacing={3}>
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
bg="rgba(255,215,0,0.15)"
|
||||||
|
borderRadius="lg"
|
||||||
|
border="1px solid rgba(255,215,0,0.3)"
|
||||||
|
>
|
||||||
|
<Icon as={Calendar} color="gold" boxSize={5} />
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
fontSize="lg"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, #FFD700, #FFA500)"
|
||||||
|
bgClip="text"
|
||||||
|
>
|
||||||
|
投资日历
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} />
|
||||||
|
<ModalBody pb={6}>
|
||||||
|
<InvestmentCalendar />
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -810,9 +567,10 @@ const HeroPanel = () => {
|
|||||||
filter="blur(50px)"
|
filter="blur(50px)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardBody p={{ base: 4, md: 5 }}>
|
<CardBody p={{ base: 3, md: 4 }}>
|
||||||
{/* 标题行:标题 + 使用说明 + 交易状态 */}
|
{/* 标题行:标题 + 使用说明 + 日历 + 交易状态 */}
|
||||||
<Flex align="center" justify="space-between" mb={4} wrap="wrap" gap={2}>
|
<Flex align="center" justify="space-between" wrap="wrap" gap={3}>
|
||||||
|
{/* 左侧:标题 + 使用说明 */}
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Heading size="lg">
|
<Heading size="lg">
|
||||||
<Text
|
<Text
|
||||||
@@ -827,9 +585,7 @@ const HeroPanel = () => {
|
|||||||
</Heading>
|
</Heading>
|
||||||
{/* 使用说明 - 弹窗 */}
|
{/* 使用说明 - 弹窗 */}
|
||||||
<InfoModal />
|
<InfoModal />
|
||||||
</HStack>
|
{/* 交易状态 */}
|
||||||
|
|
||||||
{/* 右侧:交易状态 */}
|
|
||||||
{isInTradingTime() && (
|
{isInTradingTime() && (
|
||||||
<HStack
|
<HStack
|
||||||
spacing={1.5}
|
spacing={1.5}
|
||||||
@@ -852,117 +608,10 @@ const HeroPanel = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* 内容区:指数 + 概念 */}
|
|
||||||
<Flex
|
|
||||||
direction={{ base: 'column', lg: 'row' }}
|
|
||||||
gap={4}
|
|
||||||
>
|
|
||||||
{/* 左侧:双指数横向排列 */}
|
|
||||||
<Flex
|
|
||||||
direction={{ base: 'column', md: 'row' }}
|
|
||||||
gap={4}
|
|
||||||
flex={{ lg: '0 0 58%' }}
|
|
||||||
>
|
|
||||||
{/* 上证指数 */}
|
|
||||||
<Box
|
|
||||||
flex="1"
|
|
||||||
p={4}
|
|
||||||
minH="240px"
|
|
||||||
bg="rgba(255,255,255,0.03)"
|
|
||||||
borderRadius="xl"
|
|
||||||
border="1px solid rgba(255,255,255,0.08)"
|
|
||||||
transition="all 0.3s"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255,255,255,0.05)',
|
|
||||||
borderColor: 'rgba(239,83,80,0.4)',
|
|
||||||
boxShadow: '0 4px 20px rgba(239,83,80,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CompactIndexCard indexCode="000001" indexName="上证指数" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 深证成指 */}
|
|
||||||
<Box
|
|
||||||
flex="1"
|
|
||||||
p={4}
|
|
||||||
minH="240px"
|
|
||||||
bg="rgba(255,255,255,0.03)"
|
|
||||||
borderRadius="xl"
|
|
||||||
border="1px solid rgba(255,255,255,0.08)"
|
|
||||||
transition="all 0.3s"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255,255,255,0.05)',
|
|
||||||
borderColor: 'rgba(38,166,154,0.4)',
|
|
||||||
boxShadow: '0 4px 20px rgba(38,166,154,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CompactIndexCard indexCode="399001" indexName="深证成指" />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* 右侧:热门概念 - 流动式设计 */}
|
|
||||||
<Box
|
|
||||||
flex={{ lg: '1' }}
|
|
||||||
minH={{ base: '270px', lg: '240px' }}
|
|
||||||
bg="linear-gradient(135deg, rgba(0,0,0,0.4) 0%, rgba(20,20,40,0.3) 50%, rgba(0,0,0,0.4) 100%)"
|
|
||||||
borderRadius="2xl"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255,215,0,0.15)"
|
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
|
||||||
boxShadow="inset 0 2px 20px rgba(0,0,0,0.3), 0 4px 30px rgba(0,0,0,0.2)"
|
|
||||||
_before={{
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '1px',
|
|
||||||
background: 'linear-gradient(90deg, transparent, rgba(255,215,0,0.3), transparent)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 标题栏 - 更精致的设计 */}
|
|
||||||
<Flex
|
|
||||||
position="absolute"
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
right={0}
|
|
||||||
px={4}
|
|
||||||
py={2}
|
|
||||||
bg="rgba(0,0,0,0.3)"
|
|
||||||
backdropFilter={GLASS_BLUR.sm}
|
|
||||||
borderBottom="1px solid rgba(255,255,255,0.05)"
|
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
zIndex={10}
|
|
||||||
>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Box
|
|
||||||
p={1.5}
|
|
||||||
bg="rgba(255,215,0,0.15)"
|
|
||||||
borderRadius="lg"
|
|
||||||
border="1px solid rgba(255,215,0,0.2)"
|
|
||||||
>
|
|
||||||
<Icon as={TrendingUp} color="gold" boxSize={4} />
|
|
||||||
</Box>
|
|
||||||
<VStack align="start" spacing={0}>
|
|
||||||
<Text fontSize="sm" fontWeight="bold" color="gold">
|
|
||||||
热门概念
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="10px" color="whiteAlpha.500">
|
|
||||||
实时涨跌排行
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* 流动式概念展示 */}
|
{/* 右侧:紧凑型日历 */}
|
||||||
<Box pt="52px" h="100%">
|
<CompactCalendar />
|
||||||
<FlowingConcepts />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user