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