refactor(HeroPanel): 提取日历和统计卡片组件
- CalendarCell: 日历单元格,memo 优化渲染 - CombinedCalendar: 综合日历组件,懒加载 DetailModal - HotKeywordsCloud: 热门关键词云,涨停分析 Tab 使用 - ZTStatsCards: 涨停统计卡片(连板分布、封板时间、公告驱动) - InfoModal: 使用说明弹窗 - index.js: 组件统一导出 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
// HeroPanel - 日历单元格组件
|
||||
import React, { memo } from "react";
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Tooltip,
|
||||
Icon,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flame, FileText, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { goldColors, textColors } from "../constants";
|
||||
import { getHeatColor } from "../utils";
|
||||
|
||||
/**
|
||||
* 趋势图标
|
||||
*/
|
||||
const TrendIcon = memo(({ current, previous }) => {
|
||||
if (!current || !previous) return null;
|
||||
const diff = current - previous;
|
||||
if (diff === 0) return null;
|
||||
|
||||
const isUp = diff > 0;
|
||||
return (
|
||||
<Icon
|
||||
as={isUp ? TrendingUp : TrendingDown}
|
||||
boxSize={3}
|
||||
color={isUp ? "#22c55e" : "#ef4444"}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
TrendIcon.displayName = "TrendIcon";
|
||||
|
||||
/**
|
||||
* 日历单元格 - 显示涨停数和事件数(加大尺寸)
|
||||
* 新增:连续概念连接展示(connectLeft/connectRight 表示与左右格子是否同一概念)
|
||||
*/
|
||||
const CalendarCell = memo(
|
||||
({
|
||||
date,
|
||||
ztData,
|
||||
eventCount,
|
||||
previousZtData,
|
||||
isSelected,
|
||||
isToday,
|
||||
isWeekend,
|
||||
onClick,
|
||||
connectLeft,
|
||||
connectRight,
|
||||
}) => {
|
||||
if (!date) {
|
||||
return <Box minH="75px" />;
|
||||
}
|
||||
|
||||
const hasZtData = !!ztData;
|
||||
const hasEventData = eventCount > 0;
|
||||
const ztCount = ztData?.count || 0;
|
||||
const heatColors = getHeatColor(ztCount);
|
||||
const topSector = ztData?.top_sector || "";
|
||||
|
||||
// 是否有连接线(连续概念)
|
||||
const hasConnection = connectLeft || connectRight;
|
||||
|
||||
// 周末无数据显示"休市"
|
||||
if (isWeekend && !hasZtData && !hasEventData) {
|
||||
return (
|
||||
<Box
|
||||
p={2}
|
||||
borderRadius="10px"
|
||||
bg="rgba(30, 30, 40, 0.3)"
|
||||
border="1px solid rgba(255, 255, 255, 0.03)"
|
||||
textAlign="center"
|
||||
minH="75px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="400"
|
||||
color="rgba(255, 255, 255, 0.25)"
|
||||
>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="rgba(255, 255, 255, 0.2)">
|
||||
休市
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 正常日期
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
<VStack spacing={1} align="start" p={1}>
|
||||
<Text fontWeight="bold" fontSize="md">{`${
|
||||
date.getMonth() + 1
|
||||
}月${date.getDate()}日`}</Text>
|
||||
{hasZtData && (
|
||||
<Text>
|
||||
涨停: {ztCount}家 {topSector && `| ${topSector}`}
|
||||
</Text>
|
||||
)}
|
||||
{hasEventData && <Text>未来事件: {eventCount}个</Text>}
|
||||
{!hasZtData && !hasEventData && (
|
||||
<Text color="gray.400">暂无数据</Text>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
placement="top"
|
||||
hasArrow
|
||||
bg="rgba(15, 15, 22, 0.95)"
|
||||
border="1px solid rgba(212, 175, 55, 0.3)"
|
||||
borderRadius="10px"
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
p={2}
|
||||
borderRadius="10px"
|
||||
bg={
|
||||
hasZtData
|
||||
? heatColors.bg
|
||||
: hasEventData
|
||||
? "rgba(34, 197, 94, 0.2)"
|
||||
: "rgba(40, 40, 50, 0.3)"
|
||||
}
|
||||
border={
|
||||
isSelected
|
||||
? `2px solid ${goldColors.primary}`
|
||||
: isToday
|
||||
? `2px solid ${goldColors.light}`
|
||||
: hasZtData
|
||||
? `1px solid ${heatColors.border}`
|
||||
: hasEventData
|
||||
? "1px solid rgba(34, 197, 94, 0.4)"
|
||||
: "1px solid rgba(255, 255, 255, 0.08)"
|
||||
}
|
||||
boxShadow={
|
||||
isSelected
|
||||
? `0 0 15px ${goldColors.glow}`
|
||||
: isToday
|
||||
? `0 0 10px ${goldColors.glow}`
|
||||
: "none"
|
||||
}
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
||||
borderColor: goldColors.primary,
|
||||
}}
|
||||
onClick={() => onClick && onClick(date)}
|
||||
w="full"
|
||||
minH="75px"
|
||||
>
|
||||
{/* 今天标记 */}
|
||||
{isToday && (
|
||||
<Badge
|
||||
position="absolute"
|
||||
top="2px"
|
||||
right="2px"
|
||||
bg="rgba(239, 68, 68, 0.9)"
|
||||
color="white"
|
||||
fontSize="9px"
|
||||
px={1}
|
||||
borderRadius="sm"
|
||||
>
|
||||
今天
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<VStack spacing={0.5} align="center">
|
||||
{/* 日期 */}
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight={isSelected || isToday ? "bold" : "600"}
|
||||
color={
|
||||
isSelected
|
||||
? goldColors.primary
|
||||
: isToday
|
||||
? goldColors.light
|
||||
: textColors.primary
|
||||
}
|
||||
>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
|
||||
{/* 涨停数 + 趋势 */}
|
||||
{hasZtData && (
|
||||
<HStack spacing={1} justify="center">
|
||||
<Icon as={Flame} boxSize={3} color={heatColors.text} />
|
||||
<Text fontSize="sm" fontWeight="bold" color={heatColors.text}>
|
||||
{ztCount}
|
||||
</Text>
|
||||
<TrendIcon current={ztCount} previous={previousZtData?.count} />
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 事件数 */}
|
||||
{hasEventData && (
|
||||
<HStack spacing={1} justify="center">
|
||||
<Icon as={FileText} boxSize={3} color="#22c55e" />
|
||||
<Text fontSize="sm" fontWeight="bold" color="#22c55e">
|
||||
{eventCount}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 主要板块 - 连续概念用连接样式 */}
|
||||
{hasZtData && topSector && (
|
||||
<Box
|
||||
position="relative"
|
||||
w="full"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
{/* 左连接线 */}
|
||||
{connectLeft && (
|
||||
<Box
|
||||
position="absolute"
|
||||
left="-12px"
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
w="12px"
|
||||
h="2px"
|
||||
bgGradient="linear(to-r, rgba(212,175,55,0.6), rgba(212,175,55,0.3))"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={
|
||||
hasConnection ? goldColors.primary : textColors.secondary
|
||||
}
|
||||
fontWeight={hasConnection ? "bold" : "normal"}
|
||||
noOfLines={1}
|
||||
maxW="70px"
|
||||
bg={hasConnection ? "rgba(212,175,55,0.15)" : "transparent"}
|
||||
px={hasConnection ? 1.5 : 0}
|
||||
py={hasConnection ? 0.5 : 0}
|
||||
borderRadius={hasConnection ? "full" : "none"}
|
||||
border={
|
||||
hasConnection ? "1px solid rgba(212,175,55,0.3)" : "none"
|
||||
}
|
||||
>
|
||||
{topSector}
|
||||
</Text>
|
||||
{/* 右连接线 */}
|
||||
{connectRight && (
|
||||
<Box
|
||||
position="absolute"
|
||||
right="-12px"
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
w="12px"
|
||||
h="2px"
|
||||
bgGradient="linear(to-l, rgba(212,175,55,0.6), rgba(212,175,55,0.3))"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CalendarCell.displayName = "CalendarCell";
|
||||
|
||||
export default CalendarCell;
|
||||
@@ -0,0 +1,263 @@
|
||||
// HeroPanel - 综合日历组件
|
||||
import React, { useState, useEffect, useCallback, Suspense, lazy } from "react";
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
VStack,
|
||||
Text,
|
||||
Icon,
|
||||
Center,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flame } from "lucide-react";
|
||||
import dayjs from "dayjs";
|
||||
import { GLASS_BLUR } from "@/constants/glassConfig";
|
||||
import { eventService } from "@services/eventService";
|
||||
import { getApiBase } from "@utils/apiConfig";
|
||||
import { textColors } from "../constants";
|
||||
import { formatDateStr } from "../utils";
|
||||
|
||||
// 懒加载 FullCalendar
|
||||
const FullCalendarPro = lazy(() =>
|
||||
import("@components/Calendar").then((module) => ({
|
||||
default: module.FullCalendarPro,
|
||||
}))
|
||||
);
|
||||
|
||||
/**
|
||||
* 综合日历组件 - 使用 FullCalendarPro 实现跨天事件条效果
|
||||
* @param {Object} props
|
||||
* @param {React.ComponentType} props.DetailModal - 详情弹窗组件
|
||||
*/
|
||||
const CombinedCalendar = ({ DetailModal }) => {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
|
||||
// 日历综合数据(涨停 + 事件 + 上证涨跌幅)- 使用新的综合 API
|
||||
const [calendarData, setCalendarData] = useState([]);
|
||||
const [ztDailyDetails, setZtDailyDetails] = useState({});
|
||||
const [selectedZtDetail, setSelectedZtDetail] = useState(null);
|
||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
// 加载日历综合数据(一次 API 调用获取所有数据)
|
||||
useEffect(() => {
|
||||
const loadCalendarCombinedData = async () => {
|
||||
try {
|
||||
const year = currentMonth.getFullYear();
|
||||
const month = currentMonth.getMonth() + 1;
|
||||
const response = await fetch(
|
||||
`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success && result.data) {
|
||||
// 转换为 FullCalendarPro 需要的格式
|
||||
const formattedData = result.data.map((item) => ({
|
||||
date: item.date,
|
||||
count: item.zt_count || 0,
|
||||
topSector: item.top_sector || "",
|
||||
eventCount: item.event_count || 0,
|
||||
indexChange: item.index_change,
|
||||
}));
|
||||
console.log(
|
||||
"[HeroPanel] 加载日历综合数据成功,数据条数:",
|
||||
formattedData.length
|
||||
);
|
||||
setCalendarData(formattedData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load calendar combined data:", error);
|
||||
}
|
||||
};
|
||||
loadCalendarCombinedData();
|
||||
}, [currentMonth]);
|
||||
|
||||
// 处理日期点击 - 打开弹窗
|
||||
const handleDateClick = useCallback(
|
||||
async (date) => {
|
||||
setSelectedDate(date);
|
||||
setModalOpen(true);
|
||||
setDetailLoading(true);
|
||||
|
||||
const ztDateStr = formatDateStr(date);
|
||||
const eventDateStr = dayjs(date).format("YYYY-MM-DD");
|
||||
|
||||
// 加载涨停详情
|
||||
const detail = ztDailyDetails[ztDateStr];
|
||||
if (detail?.fullData) {
|
||||
setSelectedZtDetail(detail.fullData);
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(`/data/zt/daily/${ztDateStr}.json`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSelectedZtDetail(data);
|
||||
setZtDailyDetails((prev) => ({
|
||||
...prev,
|
||||
[ztDateStr]: { ...prev[ztDateStr], fullData: data },
|
||||
}));
|
||||
} else {
|
||||
setSelectedZtDetail(null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedZtDetail(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载事件详情
|
||||
try {
|
||||
const response = await eventService.calendar.getEventsForDate(
|
||||
eventDateStr
|
||||
);
|
||||
if (response.success) {
|
||||
setSelectedEvents(response.data || []);
|
||||
} else {
|
||||
setSelectedEvents([]);
|
||||
}
|
||||
} catch {
|
||||
setSelectedEvents([]);
|
||||
}
|
||||
|
||||
setDetailLoading(false);
|
||||
},
|
||||
[ztDailyDetails]
|
||||
);
|
||||
|
||||
// 月份变化回调
|
||||
const handleMonthChange = useCallback((year, month) => {
|
||||
setCurrentMonth(new Date(year, month - 1, 1));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
bg="rgba(15, 15, 22, 0.6)"
|
||||
backdropFilter={GLASS_BLUR.md}
|
||||
borderRadius="xl"
|
||||
pt={5}
|
||||
px={5}
|
||||
pb={1}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 顶部装饰条 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
h="3px"
|
||||
bgGradient="linear(to-r, transparent, #D4AF37, #F4D03F, #D4AF37, transparent)"
|
||||
animation="shimmer 3s linear infinite"
|
||||
backgroundSize="200% 100%"
|
||||
/>
|
||||
|
||||
{/* 图例说明 - 右上角 */}
|
||||
<HStack
|
||||
spacing={3}
|
||||
position="absolute"
|
||||
top={4}
|
||||
right={4}
|
||||
flexWrap="wrap"
|
||||
justify="flex-end"
|
||||
zIndex={1}
|
||||
>
|
||||
<HStack spacing={1}>
|
||||
<Box
|
||||
w="16px"
|
||||
h="8px"
|
||||
borderRadius="sm"
|
||||
bgGradient="linear(135deg, #FFD700 0%, #FFA500 100%)"
|
||||
/>
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
热门概念
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={2.5} color="#EF4444" />
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
≥60
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={2.5} color="#F59E0B" />
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
<60
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Box
|
||||
w="12px"
|
||||
h="12px"
|
||||
borderRadius="full"
|
||||
bg="linear-gradient(135deg, #22C55E 0%, #16A34A 100%)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text fontSize="6px" fontWeight="bold" color="white">
|
||||
N
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
事件
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={0.5}>
|
||||
<Text fontSize="2xs" fontWeight="600" color="#EF4444">
|
||||
+
|
||||
</Text>
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
/
|
||||
</Text>
|
||||
<Text fontSize="2xs" fontWeight="600" color="#22C55E">
|
||||
-
|
||||
</Text>
|
||||
<Text fontSize="2xs" color={textColors.muted}>
|
||||
上证
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* FullCalendar Pro - 炫酷跨天事件条日历(懒加载) */}
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center h="300px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="gold" thickness="3px" />
|
||||
<Text color="whiteAlpha.600" fontSize="sm">
|
||||
加载日历组件...
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
}
|
||||
>
|
||||
<FullCalendarPro
|
||||
data={calendarData}
|
||||
currentMonth={currentMonth}
|
||||
onDateClick={handleDateClick}
|
||||
onMonthChange={handleMonthChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
{/* 详情弹窗 */}
|
||||
{DetailModal && (
|
||||
<DetailModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
selectedDate={selectedDate}
|
||||
ztDetail={selectedZtDetail}
|
||||
events={selectedEvents}
|
||||
loading={detailLoading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedCalendar;
|
||||
@@ -0,0 +1,122 @@
|
||||
// 热门关键词云组件
|
||||
// 用于涨停分析 Tab 显示今日热词
|
||||
|
||||
import React from "react";
|
||||
import { FireOutlined } from "@ant-design/icons";
|
||||
|
||||
/**
|
||||
* 获取关键词样式(根据排名)
|
||||
*/
|
||||
const getKeywordStyle = (index) => {
|
||||
if (index < 3) {
|
||||
return {
|
||||
fontSize: "15px",
|
||||
fontWeight: "bold",
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(255,215,0,0.3) 0%, rgba(255,165,0,0.2) 100%)",
|
||||
border: "1px solid rgba(255,215,0,0.5)",
|
||||
color: "#FFD700",
|
||||
padding: "6px 12px",
|
||||
};
|
||||
}
|
||||
if (index < 6) {
|
||||
return {
|
||||
fontSize: "14px",
|
||||
fontWeight: "600",
|
||||
background: "rgba(255,215,0,0.15)",
|
||||
border: "1px solid rgba(255,215,0,0.3)",
|
||||
color: "#D4A84B",
|
||||
padding: "4px 10px",
|
||||
};
|
||||
}
|
||||
return {
|
||||
fontSize: "13px",
|
||||
fontWeight: "normal",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
color: "#888",
|
||||
padding: "2px 8px",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 热门关键词云组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.keywords - 关键词数组 [{ name: string }]
|
||||
*/
|
||||
const HotKeywordsCloud = ({ keywords }) => {
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(255, 215, 0, 0.08) 0%, rgba(255, 140, 0, 0.05) 100%)",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(255, 215, 0, 0.2)",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 装饰线 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2px",
|
||||
background:
|
||||
"linear-gradient(to right, transparent, #FFD700, #FF8C00, #FFD700, transparent)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
marginBottom: "12px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "6px",
|
||||
background: "rgba(255,215,0,0.2)",
|
||||
borderRadius: "6px",
|
||||
}}
|
||||
>
|
||||
<FireOutlined style={{ color: "#FFD700", fontSize: "16px" }} />
|
||||
</div>
|
||||
<span style={{ fontSize: "16px", fontWeight: "bold", color: "gold" }}>
|
||||
今日热词
|
||||
</span>
|
||||
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)" }}>
|
||||
词频越高排名越前
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
||||
{keywords.map((kw, idx) => {
|
||||
const style = getKeywordStyle(idx);
|
||||
return (
|
||||
<span
|
||||
key={kw.name}
|
||||
style={{
|
||||
...style,
|
||||
borderRadius: "9999px",
|
||||
transition: "all 0.2s",
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{kw.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotKeywordsCloud;
|
||||
152
src/views/Community/components/HeroPanel/components/InfoModal.js
Normal file
152
src/views/Community/components/HeroPanel/components/InfoModal.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// HeroPanel - 使用说明弹窗组件
|
||||
import React, { useState } from "react";
|
||||
import { HStack, Icon, Text } from "@chakra-ui/react";
|
||||
import { Modal as AntModal, ConfigProvider, theme } from "antd";
|
||||
import { Info } from "lucide-react";
|
||||
import { GLASS_BLUR } from "@/constants/glassConfig";
|
||||
|
||||
/**
|
||||
* 使用说明弹窗组件
|
||||
*/
|
||||
const InfoModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const onOpen = () => setIsOpen(true);
|
||||
const onClose = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
spacing={1.5}
|
||||
px={3}
|
||||
py={1.5}
|
||||
bg="rgba(255,215,0,0.08)"
|
||||
border="1px solid rgba(255,215,0,0.2)"
|
||||
borderRadius="full"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: "rgba(255,215,0,0.15)",
|
||||
borderColor: "rgba(255,215,0,0.4)",
|
||||
transform: "scale(1.02)",
|
||||
}}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<Icon as={Info} color="gold" boxSize={4} />
|
||||
<Text fontSize="sm" color="gold" fontWeight="medium">
|
||||
使用说明
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorBgElevated: 'rgba(15,15,30,0.98)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AntModal
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={550}
|
||||
centered
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
background: 'rgba(255,215,0,0.15)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,215,0,0.3)',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Info size={20} color="gold" />
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(to right, #FFD700, #FFA500)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
事件中心使用指南
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
styles={{
|
||||
header: {
|
||||
background: 'rgba(25,25,50,0.98)',
|
||||
borderBottom: '1px solid rgba(255,215,0,0.2)',
|
||||
paddingBottom: '16px',
|
||||
},
|
||||
body: {
|
||||
background: 'linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)',
|
||||
padding: '24px',
|
||||
},
|
||||
content: {
|
||||
background: '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.3)',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 25px 80px rgba(0,0,0,0.8)',
|
||||
},
|
||||
mask: {
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
backdropFilter: GLASS_BLUR.sm,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
|
||||
📅 综合日历
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
|
||||
日历同时展示
|
||||
<span style={{ color: '#d8b4fe', fontWeight: 'bold' }}>历史涨停数据</span>
|
||||
和
|
||||
<span style={{ color: '#86efac', fontWeight: 'bold' }}>未来事件</span>
|
||||
, 点击日期查看详细信息。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
|
||||
🔥 涨停板块
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
|
||||
点击历史日期,查看当日涨停板块排行、涨停数量、涨停股票代码,帮助理解市场主线。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
|
||||
📊 未来事件
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
|
||||
点击未来日期,查看事件详情,包括
|
||||
<span style={{ color: '#67e8f9', fontWeight: 'bold' }}>背景分析</span>
|
||||
、
|
||||
<span style={{ color: '#fdba74', fontWeight: 'bold' }}>未来推演</span>
|
||||
、
|
||||
<span style={{ color: '#86efac', fontWeight: 'bold' }}>相关股票</span>
|
||||
等。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ paddingTop: '12px', borderTop: '1px solid rgba(255,215,0,0.2)' }}>
|
||||
<div style={{ fontSize: '16px', color: '#fde047', textAlign: 'center', fontWeight: '500' }}>
|
||||
💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AntModal>
|
||||
</ConfigProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoModal;
|
||||
@@ -0,0 +1,171 @@
|
||||
// 涨停统计卡片组件
|
||||
// 显示连板分布、封板时间、公告驱动统计
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* 获取连板颜色
|
||||
*/
|
||||
const getContinuousColor = (key) => {
|
||||
if (key === "4连板+") return "#ff4d4f";
|
||||
if (key === "3连板") return "#fa541c";
|
||||
if (key === "2连板") return "#fa8c16";
|
||||
return "#52c41a";
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取时间颜色
|
||||
*/
|
||||
const getTimeColor = (key) => {
|
||||
if (key === "秒板") return "#ff4d4f";
|
||||
if (key === "早盘") return "#fa8c16";
|
||||
if (key === "盘中") return "#52c41a";
|
||||
return "#888";
|
||||
};
|
||||
|
||||
/**
|
||||
* 统计卡片基础样式
|
||||
*/
|
||||
const cardStyle = {
|
||||
flex: 1,
|
||||
minWidth: "200px",
|
||||
padding: "12px",
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
};
|
||||
|
||||
/**
|
||||
* 涨停统计卡片组件
|
||||
* @param {Object} props
|
||||
* @param {Object} props.stats - 统计数据
|
||||
* @param {Object} props.stats.continuousStats - 连板分布
|
||||
* @param {Object} props.stats.timeStats - 时间分布
|
||||
* @param {number} props.stats.announcementCount - 公告驱动数
|
||||
* @param {number} props.stats.announcementRatio - 公告驱动占比
|
||||
*/
|
||||
const ZTStatsCards = ({ stats }) => {
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
|
||||
{/* 连板分布 */}
|
||||
<div style={cardStyle}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
display: "block",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
连板分布
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
|
||||
{Object.entries(stats.continuousStats).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
fontWeight: "bold",
|
||||
color: getContinuousColor(key),
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: "10px", color: "rgba(255,255,255,0.5)" }}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 涨停时间分布 */}
|
||||
<div style={cardStyle}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
display: "block",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
封板时间
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
|
||||
{Object.entries(stats.timeStats).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
fontWeight: "bold",
|
||||
color: getTimeColor(key),
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: "10px", color: "rgba(255,255,255,0.5)" }}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 公告驱动 */}
|
||||
<div
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
minWidth: "120px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
display: "block",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
公告驱动
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "baseline" }}>
|
||||
<span
|
||||
style={{ fontSize: "20px", fontWeight: "bold", color: "#A855F7" }}
|
||||
>
|
||||
{stats.announcementCount}
|
||||
</span>
|
||||
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)" }}>
|
||||
只 ({stats.announcementRatio}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZTStatsCards;
|
||||
@@ -0,0 +1,7 @@
|
||||
// HeroPanel 子组件导出
|
||||
export * from "./DetailModal";
|
||||
export { default as CalendarCell } from "./CalendarCell";
|
||||
export { default as InfoModal } from "./InfoModal";
|
||||
export { default as CombinedCalendar } from "./CombinedCalendar";
|
||||
export { default as HotKeywordsCloud } from "./HotKeywordsCloud";
|
||||
export { default as ZTStatsCards } from "./ZTStatsCards";
|
||||
Reference in New Issue
Block a user