refactor(MarketOverviewBanner): 提取常量、组件、弹窗到子模块
- constants.js: 涨跌颜色、交易时间判断、格式化函数 - components.js: MarketStatsBarCompact, CircularProgressCard, BannerStatCard - StockTop10Modal.js: TOP10 股票弹窗 - index.js: 模块统一导出 主文件从 ~440 行精简到 ~180 行 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -17,295 +17,31 @@ import {
|
|||||||
FireOutlined,
|
FireOutlined,
|
||||||
RiseOutlined,
|
RiseOutlined,
|
||||||
FallOutlined,
|
FallOutlined,
|
||||||
ThunderboltOutlined,
|
|
||||||
TrophyOutlined,
|
|
||||||
BarChartOutlined,
|
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
StockOutlined,
|
BarChartOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Modal, Table } from "antd";
|
|
||||||
import { getApiBase } from "@utils/apiConfig";
|
import { getApiBase } from "@utils/apiConfig";
|
||||||
|
|
||||||
// 涨跌颜色常量
|
// 模块化导入
|
||||||
const UP_COLOR = "#FF4D4F"; // 涨 - 红色
|
import {
|
||||||
const DOWN_COLOR = "#52C41A"; // 跌 - 绿色
|
UP_COLOR,
|
||||||
const FLAT_COLOR = "#888888"; // 平 - 灰色
|
DOWN_COLOR,
|
||||||
|
isInTradingTime,
|
||||||
/**
|
formatChg,
|
||||||
* 判断是否在交易时间内 (9:30-15:00)
|
} from "./MarketOverviewBanner/constants";
|
||||||
*/
|
import {
|
||||||
const isInTradingTime = () => {
|
MarketStatsBarCompact,
|
||||||
const now = new Date();
|
CircularProgressCard,
|
||||||
const hours = now.getHours();
|
BannerStatCard,
|
||||||
const minutes = now.getMinutes();
|
} from "./MarketOverviewBanner/components";
|
||||||
const time = hours * 60 + minutes;
|
import StockTop10Modal from "./MarketOverviewBanner/StockTop10Modal";
|
||||||
return time >= 570 && time <= 900; // 9:30-15:00
|
|
||||||
};
|
|
||||||
|
|
||||||
// 注入脉冲动画样式
|
|
||||||
if (typeof document !== "undefined") {
|
|
||||||
const styleId = "market-banner-animations";
|
|
||||||
if (!document.getElementById(styleId)) {
|
|
||||||
const styleSheet = document.createElement("style");
|
|
||||||
styleSheet.id = styleId;
|
|
||||||
styleSheet.innerText = `
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.6; transform: scale(1.1); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(styleSheet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化涨跌幅
|
|
||||||
*/
|
|
||||||
const formatChg = (val) => {
|
|
||||||
if (val === null || val === undefined) return "-";
|
|
||||||
const num = parseFloat(val);
|
|
||||||
if (isNaN(num)) return "-";
|
|
||||||
return (num >= 0 ? "+" : "") + num.toFixed(2) + "%";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 沪深实时涨跌条形图组件 - 紧凑版
|
|
||||||
*/
|
|
||||||
const MarketStatsBarCompact = ({ marketStats }) => {
|
|
||||||
if (!marketStats || marketStats.totalCount === 0) return null;
|
|
||||||
|
|
||||||
const {
|
|
||||||
risingCount = 0,
|
|
||||||
flatCount = 0,
|
|
||||||
fallingCount = 0,
|
|
||||||
totalCount = 0,
|
|
||||||
} = marketStats;
|
|
||||||
const risePercent = totalCount > 0 ? (risingCount / totalCount) * 100 : 0;
|
|
||||||
const flatPercent = totalCount > 0 ? (flatCount / totalCount) * 100 : 0;
|
|
||||||
const fallPercent = totalCount > 0 ? (fallingCount / totalCount) * 100 : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
{/* 标题 */}
|
|
||||||
<HStack justify="space-between" mb={2}>
|
|
||||||
<Text fontSize="xs" color="whiteAlpha.700" fontWeight="medium">
|
|
||||||
沪深实时涨跌
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs" color="whiteAlpha.500">
|
|
||||||
({totalCount}只)
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 进度条 */}
|
|
||||||
<Box
|
|
||||||
h="16px"
|
|
||||||
borderRadius="md"
|
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
|
||||||
mb={2}
|
|
||||||
>
|
|
||||||
<Flex h="100%">
|
|
||||||
<Box
|
|
||||||
w={`${risePercent}%`}
|
|
||||||
h="100%"
|
|
||||||
bg={UP_COLOR}
|
|
||||||
transition="width 0.5s ease"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
w={`${flatPercent}%`}
|
|
||||||
h="100%"
|
|
||||||
bg={FLAT_COLOR}
|
|
||||||
transition="width 0.5s ease"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
w={`${fallPercent}%`}
|
|
||||||
h="100%"
|
|
||||||
bg={DOWN_COLOR}
|
|
||||||
transition="width 0.5s ease"
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 数值标签 */}
|
|
||||||
<Flex justify="space-between">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box w="6px" h="6px" borderRadius="sm" bg={UP_COLOR} />
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
color={UP_COLOR}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="monospace"
|
|
||||||
>
|
|
||||||
{risingCount}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="2xs" color="whiteAlpha.500">
|
|
||||||
涨
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box w="6px" h="6px" borderRadius="sm" bg={FLAT_COLOR} />
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
color={FLAT_COLOR}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="monospace"
|
|
||||||
>
|
|
||||||
{flatCount}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="2xs" color="whiteAlpha.500">
|
|
||||||
平
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box w="6px" h="6px" borderRadius="sm" bg={DOWN_COLOR} />
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
color={DOWN_COLOR}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="monospace"
|
|
||||||
>
|
|
||||||
{fallingCount}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="2xs" color="whiteAlpha.500">
|
|
||||||
跌
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环形进度图组件 - 仿图片样式
|
|
||||||
* @param {boolean} noBorder - 是否不显示边框(用于嵌套在其他容器中)
|
|
||||||
*/
|
|
||||||
const CircularProgressCard = ({ label, value, color = "#EC4899", size = 44, highlight = false, noBorder = false }) => {
|
|
||||||
const percentage = parseFloat(value) || 0;
|
|
||||||
const strokeWidth = 3;
|
|
||||||
const radius = (size - strokeWidth) / 2;
|
|
||||||
// 270度圆弧(底部有缺口)
|
|
||||||
const arcLength = (270 / 360) * 2 * Math.PI * radius;
|
|
||||||
const progressLength = (percentage / 100) * arcLength;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
bg={noBorder ? "transparent" : "rgba(255,255,255,0.03)"}
|
|
||||||
borderRadius="lg"
|
|
||||||
px={noBorder ? 1 : 2}
|
|
||||||
py={noBorder ? 0 : 1}
|
|
||||||
border={noBorder ? "none" : "1px solid"}
|
|
||||||
borderColor={noBorder ? "transparent" : "rgba(255,255,255,0.06)"}
|
|
||||||
_hover={noBorder ? {} : {
|
|
||||||
borderColor: "rgba(255,255,255,0.12)",
|
|
||||||
bg: "rgba(255,255,255,0.05)",
|
|
||||||
}}
|
|
||||||
transition="all 0.2s"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="row"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
gap={2}
|
|
||||||
>
|
|
||||||
<Text fontSize="xs" color={highlight ? "white" : "whiteAlpha.600"} fontWeight={highlight ? "bold" : "medium"} whiteSpace="nowrap">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Box position="relative" w={`${size}px`} h={`${size}px`} flexShrink={0}>
|
|
||||||
<svg width={size} height={size} style={{ transform: "rotate(135deg)" }}>
|
|
||||||
{/* 背景圆弧 */}
|
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
fill="none"
|
|
||||||
stroke="rgba(255,255,255,0.15)"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeDasharray={`${arcLength} ${2 * Math.PI * radius}`}
|
|
||||||
/>
|
|
||||||
{/* 进度圆弧 */}
|
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
fill="none"
|
|
||||||
stroke={color}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeDasharray={`${progressLength} ${2 * Math.PI * radius}`}
|
|
||||||
style={{
|
|
||||||
transition: "stroke-dasharray 0.5s ease",
|
|
||||||
filter: `drop-shadow(0 0 4px ${color})`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/* 中心数值 */}
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
transform="translate(-50%, -50%)"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="bold"
|
|
||||||
color={color}
|
|
||||||
lineHeight="1"
|
|
||||||
textShadow={`0 0 10px ${color}50`}
|
|
||||||
>
|
|
||||||
{percentage.toFixed(1)}%
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 紧凑数据卡片 - 通栏版
|
|
||||||
*/
|
|
||||||
const BannerStatCard = ({ label, value, icon, color = "#7C3AED", highlight = false }) => (
|
|
||||||
<Box
|
|
||||||
bg="rgba(255,255,255,0.03)"
|
|
||||||
borderRadius="lg"
|
|
||||||
px={2}
|
|
||||||
py={1.5}
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255,255,255,0.06)"
|
|
||||||
_hover={{
|
|
||||||
borderColor: "rgba(255,255,255,0.12)",
|
|
||||||
bg: "rgba(255,255,255,0.05)",
|
|
||||||
}}
|
|
||||||
transition="all 0.2s"
|
|
||||||
position="relative"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<HStack spacing={1.5} mb={0.5}>
|
|
||||||
<Box color={color} fontSize="xs" opacity={0.8}>
|
|
||||||
{icon}
|
|
||||||
</Box>
|
|
||||||
<Text fontSize="2xs" color={highlight ? "white" : "whiteAlpha.600"} fontWeight={highlight ? "bold" : "medium"}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<Text
|
|
||||||
fontSize="md"
|
|
||||||
fontWeight="bold"
|
|
||||||
color={color}
|
|
||||||
lineHeight="1.2"
|
|
||||||
textShadow={`0 0 15px ${color}25`}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
const MarketOverviewBanner = () => {
|
const MarketOverviewBanner = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split("T")[0]);
|
const [selectedDate, setSelectedDate] = useState(
|
||||||
|
new Date().toISOString().split("T")[0]
|
||||||
|
);
|
||||||
const [stockModalVisible, setStockModalVisible] = useState(false);
|
const [stockModalVisible, setStockModalVisible] = useState(false);
|
||||||
const dateInputRef = useRef(null);
|
const dateInputRef = useRef(null);
|
||||||
|
|
||||||
@@ -337,7 +73,7 @@ const MarketOverviewBanner = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 日期变化时静默刷新(不显示 loading)
|
// 日期变化时静默刷新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
fetchStats(selectedDate, false);
|
fetchStats(selectedDate, false);
|
||||||
@@ -371,6 +107,7 @@ const MarketOverviewBanner = () => {
|
|||||||
if (!stats) return null;
|
if (!stats) return null;
|
||||||
|
|
||||||
const { summary, marketStats, topStocks = [] } = stats;
|
const { summary, marketStats, topStocks = [] } = stats;
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
@@ -420,9 +157,10 @@ const MarketOverviewBanner = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{/* 返回今天按钮 - 当选择的不是今天时显示 */}
|
{/* 返回今天按钮 */}
|
||||||
{selectedDate !== new Date().toISOString().split("T")[0] && (
|
{selectedDate !== today && (
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="white"
|
color="white"
|
||||||
@@ -438,12 +176,12 @@ const MarketOverviewBanner = () => {
|
|||||||
borderColor: "rgba(255, 215, 0, 0.6)",
|
borderColor: "rgba(255, 215, 0, 0.6)",
|
||||||
}}
|
}}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
onClick={() => setSelectedDate(new Date().toISOString().split("T")[0])}
|
onClick={() => setSelectedDate(today)}
|
||||||
>
|
>
|
||||||
返回今天
|
返回今天
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{/* 日期选择器 - 金色边框 */}
|
{/* 日期选择器 */}
|
||||||
<Box
|
<Box
|
||||||
as="label"
|
as="label"
|
||||||
display="flex"
|
display="flex"
|
||||||
@@ -473,7 +211,7 @@ const MarketOverviewBanner = () => {
|
|||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
onClick={handleCalendarClick}
|
onClick={handleCalendarClick}
|
||||||
max={new Date().toISOString().split("T")[0]}
|
max={today}
|
||||||
bg="transparent"
|
bg="transparent"
|
||||||
border="none"
|
border="none"
|
||||||
color="#FFD700"
|
color="#FFD700"
|
||||||
@@ -503,7 +241,7 @@ const MarketOverviewBanner = () => {
|
|||||||
<MarketStatsBarCompact marketStats={marketStats} />
|
<MarketStatsBarCompact marketStats={marketStats} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 右侧:6个指标卡片 - 1行6列 */}
|
{/* 右侧:6个指标卡片 */}
|
||||||
<Grid templateColumns="repeat(6, 1fr)" gap={1.5} flex="1">
|
<Grid templateColumns="repeat(6, 1fr)" gap={1.5} flex="1">
|
||||||
<CircularProgressCard
|
<CircularProgressCard
|
||||||
label="事件胜率"
|
label="事件胜率"
|
||||||
@@ -538,7 +276,7 @@ const MarketOverviewBanner = () => {
|
|||||||
color="#F59E0B"
|
color="#F59E0B"
|
||||||
highlight
|
highlight
|
||||||
/>
|
/>
|
||||||
{/* 关联股票 + TOP10标签 - 与 BannerStatCard 样式统一 */}
|
{/* 关联股票卡片 */}
|
||||||
<Box
|
<Box
|
||||||
bg="rgba(255,255,255,0.03)"
|
bg="rgba(255,255,255,0.03)"
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
@@ -598,126 +336,11 @@ const MarketOverviewBanner = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 股票TOP10弹窗 */}
|
{/* 股票TOP10弹窗 */}
|
||||||
<Modal
|
<StockTop10Modal
|
||||||
title={
|
visible={stockModalVisible}
|
||||||
<HStack spacing={2}>
|
onClose={() => setStockModalVisible(false)}
|
||||||
<StockOutlined style={{ color: "white" }} />
|
topStocks={topStocks}
|
||||||
<Text color="white">股票 TOP10</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
open={stockModalVisible}
|
|
||||||
onCancel={() => setStockModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={600}
|
|
||||||
closeIcon={<span style={{ color: "white" }}>×</span>}
|
|
||||||
styles={{
|
|
||||||
content: {
|
|
||||||
background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
|
|
||||||
border: "1px solid rgba(236, 72, 153, 0.3)",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
background: "transparent",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
.stock-top10-table .ant-table {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-thead > tr > th {
|
|
||||||
background: rgba(236, 72, 153, 0.1) !important;
|
|
||||||
color: rgba(255, 255, 255, 0.8) !important;
|
|
||||||
border-bottom: 1px solid rgba(236, 72, 153, 0.2) !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-tbody > tr > td {
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
|
|
||||||
padding: 10px 8px !important;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-tbody > tr:nth-child(odd) {
|
|
||||||
background: rgba(255, 255, 255, 0.02) !important;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-tbody > tr:nth-child(even) {
|
|
||||||
background: rgba(0, 0, 0, 0.1) !important;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-tbody > tr:hover > td {
|
|
||||||
background: rgba(236, 72, 153, 0.15) !important;
|
|
||||||
}
|
|
||||||
.stock-top10-table .ant-table-cell {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Table
|
|
||||||
dataSource={topStocks.slice(0, 10)}
|
|
||||||
rowKey={(record) => record.stockCode || record.stockName}
|
|
||||||
pagination={false}
|
|
||||||
size="small"
|
|
||||||
className="stock-top10-table"
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: "排名",
|
|
||||||
dataIndex: "rank",
|
|
||||||
key: "rank",
|
|
||||||
width: 60,
|
|
||||||
render: (_, __, index) => (
|
|
||||||
<Text
|
|
||||||
fontWeight="bold"
|
|
||||||
color={
|
|
||||||
index === 0
|
|
||||||
? "#FFD700"
|
|
||||||
: index === 1
|
|
||||||
? "#C0C0C0"
|
|
||||||
: index === 2
|
|
||||||
? "#CD7F32"
|
|
||||||
: "gray.400"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "股票代码",
|
|
||||||
dataIndex: "stockCode",
|
|
||||||
key: "stockCode",
|
|
||||||
width: 100,
|
|
||||||
render: (code) => (
|
|
||||||
<Text color="gray.400" fontSize="sm">
|
|
||||||
{code?.split(".")[0] || "-"}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "股票名称",
|
|
||||||
dataIndex: "stockName",
|
|
||||||
key: "stockName",
|
|
||||||
render: (name) => (
|
|
||||||
<Text color="white" fontWeight="medium">
|
|
||||||
{name || "-"}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "最大涨幅",
|
|
||||||
dataIndex: "maxChg",
|
|
||||||
key: "maxChg",
|
|
||||||
width: 100,
|
|
||||||
align: "right",
|
|
||||||
render: (val) => (
|
|
||||||
<Text
|
|
||||||
fontWeight="bold"
|
|
||||||
color={val >= 0 ? UP_COLOR : DOWN_COLOR}
|
|
||||||
>
|
|
||||||
{formatChg(val)}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// 股票 TOP10 弹窗组件
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Text, HStack } from "@chakra-ui/react";
|
||||||
|
import { StockOutlined } from "@ant-design/icons";
|
||||||
|
import { Modal, Table } from "antd";
|
||||||
|
import { UP_COLOR, DOWN_COLOR, formatChg } from "./constants";
|
||||||
|
|
||||||
|
// 弹窗内表格样式
|
||||||
|
const modalTableStyles = `
|
||||||
|
.stock-top10-table .ant-table {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-thead > tr > th {
|
||||||
|
background: rgba(236, 72, 153, 0.1) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
border-bottom: 1px solid rgba(236, 72, 153, 0.2) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
|
||||||
|
padding: 10px 8px !important;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-tbody > tr:nth-child(odd) {
|
||||||
|
background: rgba(255, 255, 255, 0.02) !important;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-tbody > tr:nth-child(even) {
|
||||||
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(236, 72, 153, 0.15) !important;
|
||||||
|
}
|
||||||
|
.stock-top10-table .ant-table-cell {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const tableColumns = [
|
||||||
|
{
|
||||||
|
title: "排名",
|
||||||
|
dataIndex: "rank",
|
||||||
|
key: "rank",
|
||||||
|
width: 60,
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Text
|
||||||
|
fontWeight="bold"
|
||||||
|
color={
|
||||||
|
index === 0
|
||||||
|
? "#FFD700"
|
||||||
|
: index === 1
|
||||||
|
? "#C0C0C0"
|
||||||
|
: index === 2
|
||||||
|
? "#CD7F32"
|
||||||
|
: "gray.400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "股票代码",
|
||||||
|
dataIndex: "stockCode",
|
||||||
|
key: "stockCode",
|
||||||
|
width: 100,
|
||||||
|
render: (code) => (
|
||||||
|
<Text color="gray.400" fontSize="sm">
|
||||||
|
{code?.split(".")[0] || "-"}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "股票名称",
|
||||||
|
dataIndex: "stockName",
|
||||||
|
key: "stockName",
|
||||||
|
render: (name) => (
|
||||||
|
<Text color="white" fontWeight="medium">
|
||||||
|
{name || "-"}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "最大涨幅",
|
||||||
|
dataIndex: "maxChg",
|
||||||
|
key: "maxChg",
|
||||||
|
width: 100,
|
||||||
|
align: "right",
|
||||||
|
render: (val) => (
|
||||||
|
<Text fontWeight="bold" color={val >= 0 ? UP_COLOR : DOWN_COLOR}>
|
||||||
|
{formatChg(val)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票 TOP10 弹窗组件
|
||||||
|
*/
|
||||||
|
const StockTop10Modal = ({ visible, onClose, topStocks = [] }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<StockOutlined style={{ color: "white" }} />
|
||||||
|
<Text color="white">股票 TOP10</Text>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
closeIcon={<span style={{ color: "white" }}>×</span>}
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
|
||||||
|
border: "1px solid rgba(236, 72, 153, 0.3)",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
background: "transparent",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<style>{modalTableStyles}</style>
|
||||||
|
<Table
|
||||||
|
dataSource={topStocks.slice(0, 10)}
|
||||||
|
rowKey={(record) => record.stockCode || record.stockName}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
className="stock-top10-table"
|
||||||
|
columns={tableColumns}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockTop10Modal;
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// MarketOverviewBanner 子组件
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Box, Text, HStack, Flex } from "@chakra-ui/react";
|
||||||
|
import { UP_COLOR, DOWN_COLOR, FLAT_COLOR } from "./constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沪深实时涨跌条形图组件 - 紧凑版
|
||||||
|
*/
|
||||||
|
export const MarketStatsBarCompact = ({ marketStats }) => {
|
||||||
|
if (!marketStats || marketStats.totalCount === 0) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
risingCount = 0,
|
||||||
|
flatCount = 0,
|
||||||
|
fallingCount = 0,
|
||||||
|
totalCount = 0,
|
||||||
|
} = marketStats;
|
||||||
|
const risePercent = totalCount > 0 ? (risingCount / totalCount) * 100 : 0;
|
||||||
|
const flatPercent = totalCount > 0 ? (flatCount / totalCount) * 100 : 0;
|
||||||
|
const fallPercent = totalCount > 0 ? (fallingCount / totalCount) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HStack justify="space-between" mb={2}>
|
||||||
|
<Text fontSize="xs" color="whiteAlpha.700" fontWeight="medium">
|
||||||
|
沪深实时涨跌
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="whiteAlpha.500">
|
||||||
|
({totalCount}只)
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
h="16px"
|
||||||
|
borderRadius="md"
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
|
<Flex h="100%">
|
||||||
|
<Box
|
||||||
|
w={`${risePercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={UP_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
w={`${flatPercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={FLAT_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
w={`${fallPercent}%`}
|
||||||
|
h="100%"
|
||||||
|
bg={DOWN_COLOR}
|
||||||
|
transition="width 0.5s ease"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex justify="space-between">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={UP_COLOR} />
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={UP_COLOR}
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="monospace"
|
||||||
|
>
|
||||||
|
{risingCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="2xs" color="whiteAlpha.500">
|
||||||
|
涨
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={FLAT_COLOR} />
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={FLAT_COLOR}
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="monospace"
|
||||||
|
>
|
||||||
|
{flatCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="2xs" color="whiteAlpha.500">
|
||||||
|
平
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w="6px" h="6px" borderRadius="sm" bg={DOWN_COLOR} />
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={DOWN_COLOR}
|
||||||
|
fontWeight="bold"
|
||||||
|
fontFamily="monospace"
|
||||||
|
>
|
||||||
|
{fallingCount}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="2xs" color="whiteAlpha.500">
|
||||||
|
跌
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环形进度图组件
|
||||||
|
*/
|
||||||
|
export const CircularProgressCard = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color = "#EC4899",
|
||||||
|
size = 44,
|
||||||
|
highlight = false,
|
||||||
|
noBorder = false,
|
||||||
|
}) => {
|
||||||
|
const percentage = parseFloat(value) || 0;
|
||||||
|
const strokeWidth = 3;
|
||||||
|
const radius = (size - strokeWidth) / 2;
|
||||||
|
const arcLength = (270 / 360) * 2 * Math.PI * radius;
|
||||||
|
const progressLength = (percentage / 100) * arcLength;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={noBorder ? "transparent" : "rgba(255,255,255,0.03)"}
|
||||||
|
borderRadius="lg"
|
||||||
|
px={noBorder ? 1 : 2}
|
||||||
|
py={noBorder ? 0 : 1}
|
||||||
|
border={noBorder ? "none" : "1px solid"}
|
||||||
|
borderColor={noBorder ? "transparent" : "rgba(255,255,255,0.06)"}
|
||||||
|
_hover={
|
||||||
|
noBorder
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
borderColor: "rgba(255,255,255,0.12)",
|
||||||
|
bg: "rgba(255,255,255,0.05)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition="all 0.2s"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={highlight ? "white" : "whiteAlpha.600"}
|
||||||
|
fontWeight={highlight ? "bold" : "medium"}
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Box position="relative" w={`${size}px`} h={`${size}px`} flexShrink={0}>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ transform: "rotate(135deg)" }}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255,255,255,0.15)"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={`${arcLength} ${2 * Math.PI * radius}`}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={`${progressLength} ${2 * Math.PI * radius}`}
|
||||||
|
style={{
|
||||||
|
transition: "stroke-dasharray 0.5s ease",
|
||||||
|
filter: `drop-shadow(0 0 4px ${color})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="50%"
|
||||||
|
left="50%"
|
||||||
|
transform="translate(-50%, -50%)"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={color}
|
||||||
|
lineHeight="1"
|
||||||
|
textShadow={`0 0 10px ${color}50`}
|
||||||
|
>
|
||||||
|
{percentage.toFixed(1)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 紧凑数据卡片
|
||||||
|
*/
|
||||||
|
export const BannerStatCard = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
color = "#7C3AED",
|
||||||
|
highlight = false,
|
||||||
|
}) => (
|
||||||
|
<Box
|
||||||
|
bg="rgba(255,255,255,0.03)"
|
||||||
|
borderRadius="lg"
|
||||||
|
px={2}
|
||||||
|
py={1.5}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255,255,255,0.06)"
|
||||||
|
_hover={{
|
||||||
|
borderColor: "rgba(255,255,255,0.12)",
|
||||||
|
bg: "rgba(255,255,255,0.05)",
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
position="relative"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<HStack spacing={1.5} mb={0.5}>
|
||||||
|
<Box color={color} fontSize="xs" opacity={0.8}>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
color={highlight ? "white" : "whiteAlpha.600"}
|
||||||
|
fontWeight={highlight ? "bold" : "medium"}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text
|
||||||
|
fontSize="md"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={color}
|
||||||
|
lineHeight="1.2"
|
||||||
|
textShadow={`0 0 15px ${color}25`}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// MarketOverviewBanner 常量定义
|
||||||
|
|
||||||
|
// 涨跌颜色常量
|
||||||
|
export const UP_COLOR = "#FF4D4F"; // 涨 - 红色
|
||||||
|
export const DOWN_COLOR = "#52C41A"; // 跌 - 绿色
|
||||||
|
export const FLAT_COLOR = "#888888"; // 平 - 灰色
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否在交易时间内 (9:30-15:00)
|
||||||
|
*/
|
||||||
|
export const isInTradingTime = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = now.getHours();
|
||||||
|
const minutes = now.getMinutes();
|
||||||
|
const time = hours * 60 + minutes;
|
||||||
|
return time >= 570 && time <= 900; // 9:30-15:00
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化涨跌幅
|
||||||
|
*/
|
||||||
|
export const formatChg = (val) => {
|
||||||
|
if (val === null || val === undefined) return "-";
|
||||||
|
const num = parseFloat(val);
|
||||||
|
if (isNaN(num)) return "-";
|
||||||
|
return (num >= 0 ? "+" : "") + num.toFixed(2) + "%";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注入脉冲动画样式
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
const styleId = "market-banner-animations";
|
||||||
|
if (!document.getElementById(styleId)) {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.id = styleId;
|
||||||
|
styleSheet.innerText = `
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.6; transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// MarketOverviewBanner 模块导出
|
||||||
|
|
||||||
|
export { UP_COLOR, DOWN_COLOR, FLAT_COLOR, isInTradingTime, formatChg } from "./constants";
|
||||||
|
export { MarketStatsBarCompact, CircularProgressCard, BannerStatCard } from "./components";
|
||||||
|
export { default as StockTop10Modal } from "./StockTop10Modal";
|
||||||
Reference in New Issue
Block a user