heropanel修改
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
|
Input,
|
||||||
|
Flex,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FireOutlined,
|
FireOutlined,
|
||||||
@@ -24,6 +26,7 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
StockOutlined,
|
StockOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
@@ -62,9 +65,8 @@ const getChgColor = (val) => {
|
|||||||
*/
|
*/
|
||||||
const WinRateGauge = ({ rate }) => {
|
const WinRateGauge = ({ rate }) => {
|
||||||
const validRate = Math.min(100, Math.max(0, rate || 0));
|
const validRate = Math.min(100, Math.max(0, rate || 0));
|
||||||
const angle = (validRate / 100) * 180; // 0-180度
|
const angle = (validRate / 100) * 180;
|
||||||
|
|
||||||
// 根据胜率确定颜色
|
|
||||||
const getGaugeColor = (r) => {
|
const getGaugeColor = (r) => {
|
||||||
if (r >= 70) return '#52C41A';
|
if (r >= 70) return '#52C41A';
|
||||||
if (r >= 50) return '#FFD700';
|
if (r >= 50) return '#FFD700';
|
||||||
@@ -74,23 +76,21 @@ const WinRateGauge = ({ rate }) => {
|
|||||||
const gaugeColor = getGaugeColor(validRate);
|
const gaugeColor = getGaugeColor(validRate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" w="100%" h="90px" overflow="hidden">
|
<Box position="relative" w="100%" h="80px" overflow="hidden">
|
||||||
{/* 仪表盘背景 */}
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="0"
|
||||||
left="50%"
|
left="50%"
|
||||||
transform="translateX(-50%)"
|
transform="translateX(-50%)"
|
||||||
w="140px"
|
w="120px"
|
||||||
h="70px"
|
h="60px"
|
||||||
borderTopLeftRadius="70px"
|
borderTopLeftRadius="60px"
|
||||||
borderTopRightRadius="70px"
|
borderTopRightRadius="60px"
|
||||||
bg="rgba(255,255,255,0.05)"
|
bg="rgba(255,255,255,0.05)"
|
||||||
border="3px solid rgba(255,255,255,0.1)"
|
border="3px solid rgba(255,255,255,0.1)"
|
||||||
borderBottom="none"
|
borderBottom="none"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{/* 填充弧 */}
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="0"
|
||||||
@@ -103,13 +103,12 @@ const WinRateGauge = ({ rate }) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 指针 */}
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="3px"
|
bottom="3px"
|
||||||
left="50%"
|
left="50%"
|
||||||
w="3px"
|
w="2px"
|
||||||
h="50px"
|
h="42px"
|
||||||
bg={gaugeColor}
|
bg={gaugeColor}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
transformOrigin="bottom center"
|
transformOrigin="bottom center"
|
||||||
@@ -118,36 +117,33 @@ const WinRateGauge = ({ rate }) => {
|
|||||||
transition="transform 0.5s ease-out"
|
transition="transform 0.5s ease-out"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 中心点 */}
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="0"
|
||||||
left="50%"
|
left="50%"
|
||||||
transform="translateX(-50%)"
|
transform="translateX(-50%)"
|
||||||
w="12px"
|
w="10px"
|
||||||
h="12px"
|
h="10px"
|
||||||
bg={gaugeColor}
|
bg={gaugeColor}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
boxShadow={`0 0 8px ${gaugeColor}`}
|
boxShadow={`0 0 8px ${gaugeColor}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数值显示 */}
|
|
||||||
<VStack
|
<VStack
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="15px"
|
bottom="12px"
|
||||||
left="50%"
|
left="50%"
|
||||||
transform="translateX(-50%)"
|
transform="translateX(-50%)"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<Text fontSize="2xl" fontWeight="bold" color={gaugeColor} lineHeight="1">
|
<Text fontSize="xl" fontWeight="bold" color={gaugeColor} lineHeight="1">
|
||||||
{validRate.toFixed(0)}%
|
{validRate.toFixed(0)}%
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="gray.500">胜率</Text>
|
<Text fontSize="2xs" color="gray.500">胜率</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* 刻度标签 */}
|
<Text position="absolute" bottom="0" left="12px" fontSize="2xs" color="gray.600">0</Text>
|
||||||
<Text position="absolute" bottom="0" left="15px" fontSize="2xs" color="gray.600">0</Text>
|
<Text position="absolute" bottom="0" right="12px" fontSize="2xs" color="gray.600">100</Text>
|
||||||
<Text position="absolute" bottom="0" right="15px" fontSize="2xs" color="gray.600">100</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -159,21 +155,21 @@ const CompactStatCard = ({ label, value, icon, color = '#FFD700', subText }) =>
|
|||||||
<Box
|
<Box
|
||||||
bg="rgba(0,0,0,0.25)"
|
bg="rgba(0,0,0,0.25)"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
p={2.5}
|
p={2}
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="rgba(255,215,0,0.12)"
|
borderColor="rgba(255,215,0,0.12)"
|
||||||
_hover={{ borderColor: 'rgba(255,215,0,0.25)' }}
|
_hover={{ borderColor: 'rgba(255,215,0,0.25)' }}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
<HStack spacing={1.5} mb={1}>
|
<HStack spacing={1.5} mb={0.5}>
|
||||||
<Box color={color} fontSize="xs">
|
<Box color={color} fontSize="xs">
|
||||||
{icon}
|
{icon}
|
||||||
</Box>
|
</Box>
|
||||||
<Text fontSize="xs" color="gray.500" fontWeight="medium">
|
<Text fontSize="2xs" color="gray.500" fontWeight="medium">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="lg" fontWeight="bold" color={color} lineHeight="1.2">
|
<Text fontSize="md" fontWeight="bold" color={color} lineHeight="1.2">
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
{subText && (
|
{subText && (
|
||||||
@@ -197,7 +193,7 @@ const TopEventItem = ({ event, rank }) => {
|
|||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
py={1.5}
|
py={1}
|
||||||
px={2}
|
px={2}
|
||||||
bg="rgba(0,0,0,0.2)"
|
bg="rgba(0,0,0,0.2)"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -240,7 +236,7 @@ const TopStockItem = ({ stock, rank }) => {
|
|||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
py={1.5}
|
py={1}
|
||||||
px={2}
|
px={2}
|
||||||
bg="rgba(0,0,0,0.2)"
|
bg="rgba(0,0,0,0.2)"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -257,7 +253,7 @@ const TopStockItem = ({ stock, rank }) => {
|
|||||||
>
|
>
|
||||||
{rank}
|
{rank}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fontSize="xs" color="gray.400" w="60px">
|
<Text fontSize="xs" color="gray.400" w="55px">
|
||||||
{stock.stockCode?.split('.')[0] || '-'}
|
{stock.stockCode?.split('.')[0] || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="gray.300" flex="1" noOfLines={1}>
|
<Text fontSize="xs" color="gray.300" flex="1" noOfLines={1}>
|
||||||
@@ -275,13 +271,15 @@ const EventDailyStats = () => {
|
|||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [selectedDate, setSelectedDate] = useState('');
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
const fetchStats = useCallback(async (dateStr = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const apiBase = getApiBase();
|
const apiBase = getApiBase();
|
||||||
const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=1`);
|
const dateParam = dateStr ? `&date=${dateStr}` : '';
|
||||||
|
const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=1${dateParam}`);
|
||||||
if (!response.ok) throw new Error('获取数据失败');
|
if (!response.ok) throw new Error('获取数据失败');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success || data.code === 200) {
|
if (data.success || data.code === 200) {
|
||||||
@@ -290,7 +288,7 @@ const EventDailyStats = () => {
|
|||||||
throw new Error(data.message || '数据格式错误');
|
throw new Error(data.message || '数据格式错误');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取当日事件统计失败:', err);
|
console.error('获取事件统计失败:', err);
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -298,10 +296,22 @@ const EventDailyStats = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats(selectedDate);
|
||||||
const interval = setInterval(fetchStats, 5 * 60 * 1000);
|
}, [fetchStats, selectedDate]);
|
||||||
|
|
||||||
|
// 自动刷新(仅当选择今天时)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDate) {
|
||||||
|
const interval = setInterval(() => fetchStats(''), 5 * 60 * 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchStats]);
|
}
|
||||||
|
}, [selectedDate, fetchStats]);
|
||||||
|
|
||||||
|
const handleDateChange = (e) => {
|
||||||
|
setSelectedDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = !selectedDate;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -312,7 +322,6 @@ const EventDailyStats = () => {
|
|||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="rgba(255, 215, 0, 0.15)"
|
borderColor="rgba(255, 215, 0, 0.15)"
|
||||||
h="100%"
|
h="100%"
|
||||||
minH="380px"
|
|
||||||
>
|
>
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Spinner size="md" color="yellow.400" thickness="3px" />
|
<Spinner size="md" color="yellow.400" thickness="3px" />
|
||||||
@@ -330,7 +339,6 @@ const EventDailyStats = () => {
|
|||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="rgba(255, 215, 0, 0.15)"
|
borderColor="rgba(255, 215, 0, 0.15)"
|
||||||
h="100%"
|
h="100%"
|
||||||
minH="380px"
|
|
||||||
>
|
>
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<VStack spacing={2}>
|
<VStack spacing={2}>
|
||||||
@@ -354,6 +362,8 @@ const EventDailyStats = () => {
|
|||||||
position="relative"
|
position="relative"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
h="100%"
|
h="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
>
|
>
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
<Box
|
<Box
|
||||||
@@ -366,8 +376,9 @@ const EventDailyStats = () => {
|
|||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 标题 */}
|
{/* 标题行 */}
|
||||||
<HStack spacing={2} mb={2}>
|
<Flex justify="space-between" align="center" mb={2}>
|
||||||
|
<HStack spacing={2}>
|
||||||
<Box
|
<Box
|
||||||
w="3px"
|
w="3px"
|
||||||
h="16px"
|
h="16px"
|
||||||
@@ -375,14 +386,43 @@ const EventDailyStats = () => {
|
|||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
/>
|
/>
|
||||||
<Text fontSize="sm" fontWeight="bold" color="white" letterSpacing="wide">
|
<Text fontSize="sm" fontWeight="bold" color="white" letterSpacing="wide">
|
||||||
今日统计
|
{isToday ? '今日统计' : '历史统计'}
|
||||||
</Text>
|
</Text>
|
||||||
|
{isToday && (
|
||||||
<Badge colorScheme="yellow" variant="subtle" fontSize="2xs" px={1.5}>
|
<Badge colorScheme="yellow" variant="subtle" fontSize="2xs" px={1.5}>
|
||||||
实时
|
实时
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<CalendarOutlined style={{ color: '#FFD700', fontSize: '12px' }} />
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
size="xs"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
max={new Date().toISOString().split('T')[0]}
|
||||||
|
bg="rgba(0,0,0,0.3)"
|
||||||
|
border="1px solid rgba(255,215,0,0.2)"
|
||||||
|
borderRadius="md"
|
||||||
|
color="white"
|
||||||
|
fontSize="xs"
|
||||||
|
w="110px"
|
||||||
|
h="24px"
|
||||||
|
_hover={{ borderColor: 'rgba(255,215,0,0.4)' }}
|
||||||
|
_focus={{ borderColor: '#FFD700', boxShadow: 'none' }}
|
||||||
|
css={{
|
||||||
|
'&::-webkit-calendar-picker-indicator': {
|
||||||
|
filter: 'invert(1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<VStack spacing={2} align="stretch">
|
{/* 内容区域 - 使用 flex: 1 填充剩余空间 */}
|
||||||
|
<VStack spacing={2} align="stretch" flex="1">
|
||||||
{/* 胜率仪表盘 */}
|
{/* 胜率仪表盘 */}
|
||||||
<WinRateGauge rate={summary?.positiveRate || 0} />
|
<WinRateGauge rate={summary?.positiveRate || 0} />
|
||||||
|
|
||||||
@@ -393,77 +433,78 @@ const EventDailyStats = () => {
|
|||||||
value={summary?.totalEvents || 0}
|
value={summary?.totalEvents || 0}
|
||||||
icon={<FireOutlined />}
|
icon={<FireOutlined />}
|
||||||
color="#FFD700"
|
color="#FFD700"
|
||||||
subText="今日追踪"
|
subText="追踪中"
|
||||||
/>
|
/>
|
||||||
<CompactStatCard
|
<CompactStatCard
|
||||||
label="关联股票"
|
label="关联股票"
|
||||||
value={summary?.totalStocks || 0}
|
value={summary?.totalStocks || 0}
|
||||||
icon={<StockOutlined />}
|
icon={<StockOutlined />}
|
||||||
color="#1890FF"
|
color="#1890FF"
|
||||||
subText="去重统计"
|
subText="去重"
|
||||||
/>
|
/>
|
||||||
<CompactStatCard
|
<CompactStatCard
|
||||||
label="平均超额"
|
label="平均超额"
|
||||||
value={formatChg(summary?.avgChg)}
|
value={formatChg(summary?.avgChg)}
|
||||||
icon={<RiseOutlined />}
|
icon={<RiseOutlined />}
|
||||||
color={getChgColor(summary?.avgChg)}
|
color={getChgColor(summary?.avgChg)}
|
||||||
subText="关联股票"
|
|
||||||
/>
|
/>
|
||||||
<CompactStatCard
|
<CompactStatCard
|
||||||
label="最大超额"
|
label="最大超额"
|
||||||
value={formatChg(summary?.maxChg)}
|
value={formatChg(summary?.maxChg)}
|
||||||
icon={<ThunderboltOutlined />}
|
icon={<ThunderboltOutlined />}
|
||||||
color="#FF4D4F"
|
color="#FF4D4F"
|
||||||
subText="单事件最佳"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 分割线 */}
|
{/* 分割线 */}
|
||||||
<Box h="1px" bg="rgba(255,215,0,0.1)" />
|
<Box h="1px" bg="rgba(255,215,0,0.1)" flexShrink={0} />
|
||||||
|
|
||||||
{/* TOP 表现 - Tab 切换 */}
|
{/* TOP 表现 - Tab 切换,flex: 1 填充剩余空间 */}
|
||||||
<Box flex="1" overflow="hidden">
|
<Box flex="1" display="flex" flexDirection="column" minH={0}>
|
||||||
<Tabs
|
<Tabs
|
||||||
variant="soft-rounded"
|
variant="soft-rounded"
|
||||||
colorScheme="yellow"
|
colorScheme="yellow"
|
||||||
size="sm"
|
size="sm"
|
||||||
index={activeTab}
|
index={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
flex="1"
|
||||||
>
|
>
|
||||||
<TabList mb={2}>
|
<TabList mb={1} flexShrink={0}>
|
||||||
<Tab
|
<Tab
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
py={1}
|
py={1}
|
||||||
px={3}
|
px={2}
|
||||||
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
|
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
>
|
>
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<TrophyOutlined style={{ fontSize: '11px' }} />
|
<TrophyOutlined style={{ fontSize: '10px' }} />
|
||||||
<Text>事件TOP10</Text>
|
<Text>事件TOP10</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
py={1}
|
py={1}
|
||||||
px={3}
|
px={2}
|
||||||
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
|
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
>
|
>
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<StockOutlined style={{ fontSize: '11px' }} />
|
<StockOutlined style={{ fontSize: '10px' }} />
|
||||||
<Text>股票TOP10</Text>
|
<Text>股票TOP10</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels flex="1" minH={0}>
|
||||||
{/* 事件 TOP10 */}
|
{/* 事件 TOP10 */}
|
||||||
<TabPanel p={0}>
|
<TabPanel p={0} h="100%">
|
||||||
<VStack
|
<VStack
|
||||||
spacing={1}
|
spacing={1}
|
||||||
align="stretch"
|
align="stretch"
|
||||||
maxH="195px"
|
h="100%"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
pr={1}
|
pr={1}
|
||||||
css={{
|
css={{
|
||||||
@@ -485,11 +526,11 @@ const EventDailyStats = () => {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* 股票 TOP10 */}
|
{/* 股票 TOP10 */}
|
||||||
<TabPanel p={0}>
|
<TabPanel p={0} h="100%">
|
||||||
<VStack
|
<VStack
|
||||||
spacing={1}
|
spacing={1}
|
||||||
align="stretch"
|
align="stretch"
|
||||||
maxH="195px"
|
h="100%"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
pr={1}
|
pr={1}
|
||||||
css={{
|
css={{
|
||||||
|
|||||||
Reference in New Issue
Block a user