diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js
index 42f9ee23..2082c702 100644
--- a/src/views/Community/components/ThemeCometChart.js
+++ b/src/views/Community/components/ThemeCometChart.js
@@ -4,7 +4,7 @@
* Y轴:板块热度(涨停家数)
* 支持时间滑动条查看历史数据
*/
-import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import React, { useState, useEffect, useMemo, useCallback } from "react";
import {
Box,
Text,
@@ -13,178 +13,34 @@ import {
Spinner,
Center,
useToast,
- Modal,
- ModalOverlay,
- ModalContent,
- ModalHeader,
- ModalBody,
- ModalCloseButton,
useDisclosure,
- Table,
- Thead,
- Tbody,
- Tr,
- Th,
- Td,
- Badge,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
Tooltip as ChakraTooltip,
-} from '@chakra-ui/react';
-import ReactECharts from 'echarts-for-react';
-import { ThunderboltOutlined, CalendarOutlined, StockOutlined, CaretUpFilled } from '@ant-design/icons';
-import { getApiBase } from '@utils/apiConfig';
+} from "@chakra-ui/react";
+import ReactECharts from "echarts-for-react";
+import {
+ ThunderboltOutlined,
+ CalendarOutlined,
+ CaretUpFilled,
+} from "@ant-design/icons";
+import { getApiBase } from "@utils/apiConfig";
-// 板块状态配置
-const STATUS_CONFIG = {
- rising: { name: '主升', color: '#FF4D4F' },
- declining: { name: '退潮', color: '#52C41A' },
- lurking: { name: '潜伏', color: '#1890FF' },
- clustering: { name: '抱团', color: '#722ED1' },
-};
-
-/**
- * 生成 ECharts 配置
- */
-const generateChartOption = (themes) => {
- if (!themes || themes.length === 0) return {};
-
- const groupedData = {
- 主升: [],
- 退潮: [],
- 潜伏: [],
- 抱团: [],
- };
-
- themes.forEach((theme) => {
- const statusName = STATUS_CONFIG[theme.status]?.name || '抱团';
- groupedData[statusName].push({
- name: theme.label,
- value: [theme.x, theme.y],
- countTrend: theme.countTrend,
- boardTrend: theme.boardTrend,
- themeColor: theme.color,
- history: theme.history,
- });
- });
-
- const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({
- name: config.name,
- type: 'scatter',
- data: groupedData[config.name] || [],
- symbolSize: (data) => Math.max(25, Math.min(70, data[1] * 3.5)),
- itemStyle: {
- color: (params) => params.data.themeColor || config.color,
- shadowBlur: 12,
- shadowColor: (params) => params.data.themeColor || config.color,
- opacity: 0.85,
- },
- emphasis: {
- itemStyle: { opacity: 1, shadowBlur: 25 },
- },
- label: {
- show: true,
- formatter: (params) => params.data.name,
- position: 'right',
- color: '#fff',
- fontSize: 14,
- fontWeight: 'bold',
- textShadowColor: 'rgba(0,0,0,0.8)',
- textShadowBlur: 4,
- },
- }));
-
- const maxX = Math.max(...themes.map((t) => t.x), 5) + 1;
- const maxY = Math.max(...themes.map((t) => t.y), 10) + 3;
-
- return {
- backgroundColor: 'transparent',
- tooltip: {
- trigger: 'item',
- backgroundColor: 'rgba(15, 15, 30, 0.95)',
- borderColor: 'rgba(255, 215, 0, 0.3)',
- borderWidth: 1,
- textStyle: { color: '#fff' },
- formatter: (params) => {
- const { name, value, countTrend, boardTrend, history } = params.data;
- const countTrendText = countTrend > 0 ? `+${countTrend}` : countTrend;
- const boardTrendText = boardTrend > 0 ? `+${boardTrend}` : boardTrend;
- const countIcon = countTrend > 0 ? '🔥' : countTrend < 0 ? '❄️' : '➡️';
-
- const historyText = (history || [])
- .slice(0, 5)
- .map((h) => `${h.date?.slice(5) || ''}: ${h.count}家/${h.maxBoard}板`)
- .join('
');
-
- return `
-
- ${name} ${countIcon}
-
-
- 涨停家数:
- ${value[1]}家
- (${countTrendText})
-
-
- 最高连板:
- ${value[0]}板
- (${boardTrendText})
-
-
-
近5日趋势:
-
${historyText}
-
- 点击查看详情
- `;
- },
- },
- legend: {
- show: false,
- },
- grid: {
- left: '10%',
- right: '8%',
- top: '12%',
- bottom: '8%',
- containLabel: true,
- },
- xAxis: {
- type: 'value',
- name: '辨识度(最高板)',
- nameLocation: 'middle',
- nameGap: 28,
- nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 },
- min: 0,
- max: maxX,
- interval: 1,
- axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
- axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12, formatter: '{value}板' },
- splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
- },
- yAxis: {
- type: 'value',
- name: '热度(家数)',
- nameLocation: 'middle',
- nameGap: 40,
- nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 },
- min: 0,
- max: maxY,
- axisLine: { show: false },
- axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 },
- splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
- },
- series,
- };
-};
+// 模块化导入
+import {
+ STATUS_CONFIG,
+ generateChartOption,
+ ThemeDetailModal,
+} from "./ThemeCometChart/index";
/**
* ThemeCometChart 主组件
*/
const ThemeCometChart = ({ onThemeSelect }) => {
const [loading, setLoading] = useState(true);
- const [allDatesData, setAllDatesData] = useState({}); // 缓存所有日期的数据
+ const [allDatesData, setAllDatesData] = useState({});
const [availableDates, setAvailableDates] = useState([]);
const [selectedTheme, setSelectedTheme] = useState(null);
const [sliderIndex, setSliderIndex] = useState(0);
@@ -197,7 +53,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
setLoading(true);
try {
const apiBase = getApiBase();
- // 先获取最新数据,拿到可用日期列表
const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`);
const result = await response.json();
@@ -205,13 +60,12 @@ const ThemeCometChart = ({ onThemeSelect }) => {
const dates = result.data.availableDates || [];
setAvailableDates(dates);
- // 缓存第一个日期(最新)的数据
const latestDate = dates[0]?.date;
const dataCache = {};
if (latestDate) {
dataCache[latestDate] = {
themes: result.data.themes || [],
- currentDate: result.data.currentDate || '',
+ currentDate: result.data.currentDate || "",
totalLimitUp: result.data.totalLimitUp || 0,
totalEvents: result.data.totalEvents || 0,
indexChange: result.data.indexChange || 0,
@@ -222,13 +76,15 @@ const ThemeCometChart = ({ onThemeSelect }) => {
const otherDates = dates.slice(1);
const promises = otherDates.map(async (dateInfo) => {
try {
- const res = await fetch(`${apiBase}/api/v1/zt/theme-scatter?date=${dateInfo.date}&days=5`);
+ const res = await fetch(
+ `${apiBase}/api/v1/zt/theme-scatter?date=${dateInfo.date}&days=5`
+ );
const data = await res.json();
if (data.success && data.data) {
return {
date: dateInfo.date,
themes: data.data.themes || [],
- currentDate: data.data.currentDate || '',
+ currentDate: data.data.currentDate || "",
totalLimitUp: data.data.totalLimitUp || 0,
totalEvents: data.data.totalEvents || 0,
indexChange: data.data.indexChange || 0,
@@ -254,24 +110,27 @@ const ThemeCometChart = ({ onThemeSelect }) => {
});
setAllDatesData(dataCache);
- setSliderIndex(0); // 默认显示最新日期
+ setSliderIndex(0);
} else {
- throw new Error(result.error || '加载失败');
+ throw new Error(result.error || "加载失败");
}
} catch (error) {
- console.error('加载题材数据失败:', error);
- toast({ title: '加载数据失败', description: error.message, status: 'error', duration: 3000 });
+ console.error("加载题材数据失败:", error);
+ toast({
+ title: "加载数据失败",
+ description: error.message,
+ status: "error",
+ duration: 3000,
+ });
} finally {
setLoading(false);
}
}, [toast]);
- // 初始加载所有数据
useEffect(() => {
loadAllData();
}, [loadAllData]);
- // 滑动条变化时实时切换数据
const handleSliderChange = useCallback((value) => {
setSliderIndex(value);
}, []);
@@ -280,19 +139,24 @@ const ThemeCometChart = ({ onThemeSelect }) => {
const currentDateStr = availableDates[sliderIndex]?.date;
const currentData = allDatesData[currentDateStr] || {
themes: [],
- currentDate: '',
+ currentDate: "",
totalLimitUp: 0,
totalEvents: 0,
- indexChange: 0
+ indexChange: 0,
};
const isCurrentDateLoaded = currentDateStr && allDatesData[currentDateStr];
- const chartOption = useMemo(() => generateChartOption(currentData.themes), [currentData.themes]);
+ const chartOption = useMemo(
+ () => generateChartOption(currentData.themes),
+ [currentData.themes]
+ );
const handleChartClick = useCallback(
(params) => {
if (params.data) {
- const theme = currentData.themes.find((t) => t.label === params.data.name);
+ const theme = currentData.themes.find(
+ (t) => t.label === params.data.name
+ );
if (theme) {
setSelectedTheme(theme);
onOpen();
@@ -302,10 +166,13 @@ const ThemeCometChart = ({ onThemeSelect }) => {
[currentData.themes, onOpen]
);
- const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]);
+ const onChartEvents = useMemo(
+ () => ({ click: handleChartClick }),
+ [handleChartClick]
+ );
- // 当前滑动条对应的日期
- const currentSliderDate = availableDates[sliderIndex]?.formatted || currentData.currentDate;
+ const currentSliderDate =
+ availableDates[sliderIndex]?.formatted || currentData.currentDate;
if (loading && Object.keys(allDatesData).length === 0) {
return (
@@ -327,30 +194,36 @@ const ThemeCometChart = ({ onThemeSelect }) => {
h="100%"
minH="350px"
>
- {/* 标题栏 - 两行布局 */}
+ {/* 标题栏 */}
- {/* 第一行:日期 + 图例 */}
-
+
{currentSliderDate || currentData.currentDate}
- {/* 图例 */}
{Object.values(STATUS_CONFIG).map((status) => (
- {status.name}
+
+ {status.name}
+
))}
- {/* 第二行:标题 + 热度 + 事件 */}
-
-
+
+
@@ -360,14 +233,24 @@ const ThemeCometChart = ({ onThemeSelect }) => {
{loading && }
-
- 热度
- {currentData.totalLimitUp}
+
+
+ 热度
+
+
+ {currentData.totalLimitUp}
+
-
- 事件
- {currentData.totalEvents}
+
+
+ 事件
+
+
+ {currentData.totalEvents}
+
@@ -379,15 +262,17 @@ const ThemeCometChart = ({ onThemeSelect }) => {
- 加载中...
+
+ 加载中...
+
) : currentData.themes.length > 0 ? (
) : (
@@ -401,7 +286,8 @@ const ThemeCometChart = ({ onThemeSelect }) => {
- {availableDates[availableDates.length - 1]?.formatted?.slice(5) || ''}
+ {availableDates[availableDates.length - 1]?.formatted?.slice(5) ||
+ ""}
{
bg="#FFD700"
border="2px solid"
borderColor="orange.400"
- _focus={{ boxShadow: '0 0 0 3px rgba(255,215,0,0.3)' }}
+ _focus={{ boxShadow: "0 0 0 3px rgba(255,215,0,0.3)" }}
/>
- {availableDates[0]?.formatted?.slice(5) || ''}
+ {availableDates[0]?.formatted?.slice(5) || ""}
)}
{/* 详情弹窗 */}
-
-
-
-
- {selectedTheme?.label} - 近5日趋势
-
-
-
- {selectedTheme && (
-
-
- 历史数据
-
-
-
- | 日期 |
- 涨停家数 |
- 最高连板 |
- 变化 |
-
-
-
- {selectedTheme.history?.map((h, idx) => {
- const prev = selectedTheme.history[idx + 1];
- const countChange = prev ? h.count - prev.count : 0;
- return (
-
- | {h.date} |
- {h.count}家 |
- {h.maxBoard}板 |
-
- {countChange !== 0 && (
- 0 ? 'green' : 'red'}>
- {countChange > 0 ? '+' : ''}{countChange}
-
- )}
- |
-
- );
- })}
-
-
-
-
-
-
- 涨停股票({selectedTheme.stocks?.length || 0}只)
-
-
-
-
-
- | 代码 |
- 名称 |
- 连板 |
-
-
-
- {selectedTheme.stocks?.slice(0, 20).map((stock) => (
-
- | {stock.scode} |
- {stock.sname} |
-
- = 3 ? 'red' : 'gray'}>
- {stock.continuous_days}板
-
- |
-
- ))}
-
-
-
-
-
- {selectedTheme.matchedSectors?.length > 0 && (
-
- 匹配板块
-
- {selectedTheme.matchedSectors.map((sector) => (
-
- {sector}
-
- ))}
-
-
- )}
-
- )}
-
-
-
+
);
};
diff --git a/src/views/Community/components/ThemeCometChart/ThemeDetailModal.js b/src/views/Community/components/ThemeCometChart/ThemeDetailModal.js
new file mode 100644
index 00000000..c24341c1
--- /dev/null
+++ b/src/views/Community/components/ThemeCometChart/ThemeDetailModal.js
@@ -0,0 +1,150 @@
+// 题材详情弹窗组件
+
+import React from "react";
+import {
+ Box,
+ Text,
+ VStack,
+ HStack,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ Badge,
+} from "@chakra-ui/react";
+
+/**
+ * 题材详情弹窗
+ */
+const ThemeDetailModal = ({ isOpen, onClose, theme }) => {
+ if (!theme) return null;
+
+ return (
+
+
+
+ {theme.label} - 近5日趋势
+
+
+
+ {/* 历史数据表格 */}
+
+
+ 历史数据
+
+
+
+
+ | 日期 |
+
+ 涨停家数
+ |
+
+ 最高连板
+ |
+ 变化 |
+
+
+
+ {theme.history?.map((h, idx) => {
+ const prev = theme.history[idx + 1];
+ const countChange = prev ? h.count - prev.count : 0;
+ return (
+
+ | {h.date} |
+
+ {h.count}家
+ |
+
+ {h.maxBoard}板
+ |
+
+ {countChange !== 0 && (
+ 0 ? "green" : "red"}
+ >
+ {countChange > 0 ? "+" : ""}
+ {countChange}
+
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+ {/* 涨停股票表格 */}
+
+
+ 涨停股票({theme.stocks?.length || 0}只)
+
+
+
+
+
+ | 代码 |
+ 名称 |
+ 连板 |
+
+
+
+ {theme.stocks?.slice(0, 20).map((stock) => (
+
+ |
+ {stock.scode}
+ |
+ {stock.sname} |
+
+ = 3 ? "red" : "gray"
+ }
+ >
+ {stock.continuous_days}板
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* 匹配板块 */}
+ {theme.matchedSectors?.length > 0 && (
+
+
+ 匹配板块
+
+
+ {theme.matchedSectors.map((sector) => (
+
+ {sector}
+
+ ))}
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default ThemeDetailModal;
diff --git a/src/views/Community/components/ThemeCometChart/chartOptions.js b/src/views/Community/components/ThemeCometChart/chartOptions.js
new file mode 100644
index 00000000..3aa9dd45
--- /dev/null
+++ b/src/views/Community/components/ThemeCometChart/chartOptions.js
@@ -0,0 +1,143 @@
+// ThemeCometChart ECharts 配置生成
+
+import { STATUS_CONFIG } from "./constants";
+
+/**
+ * 生成 ECharts 配置
+ * @param {Array} themes - 题材数据列表
+ * @returns {Object} - ECharts 配置对象
+ */
+export const generateChartOption = (themes) => {
+ if (!themes || themes.length === 0) return {};
+
+ const groupedData = {
+ 主升: [],
+ 退潮: [],
+ 潜伏: [],
+ 抱团: [],
+ };
+
+ themes.forEach((theme) => {
+ const statusName = STATUS_CONFIG[theme.status]?.name || "抱团";
+ groupedData[statusName].push({
+ name: theme.label,
+ value: [theme.x, theme.y],
+ countTrend: theme.countTrend,
+ boardTrend: theme.boardTrend,
+ themeColor: theme.color,
+ history: theme.history,
+ });
+ });
+
+ const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({
+ name: config.name,
+ type: "scatter",
+ data: groupedData[config.name] || [],
+ symbolSize: (data) => Math.max(25, Math.min(70, data[1] * 3.5)),
+ itemStyle: {
+ color: (params) => params.data.themeColor || config.color,
+ shadowBlur: 12,
+ shadowColor: (params) => params.data.themeColor || config.color,
+ opacity: 0.85,
+ },
+ emphasis: {
+ itemStyle: { opacity: 1, shadowBlur: 25 },
+ },
+ label: {
+ show: true,
+ formatter: (params) => params.data.name,
+ position: "right",
+ color: "#fff",
+ fontSize: 14,
+ fontWeight: "bold",
+ textShadowColor: "rgba(0,0,0,0.8)",
+ textShadowBlur: 4,
+ },
+ }));
+
+ const maxX = Math.max(...themes.map((t) => t.x), 5) + 1;
+ const maxY = Math.max(...themes.map((t) => t.y), 10) + 3;
+
+ return {
+ backgroundColor: "transparent",
+ tooltip: {
+ trigger: "item",
+ backgroundColor: "rgba(15, 15, 30, 0.95)",
+ borderColor: "rgba(255, 215, 0, 0.3)",
+ borderWidth: 1,
+ textStyle: { color: "#fff" },
+ formatter: (params) => {
+ const { name, value, countTrend, boardTrend, history } = params.data;
+ const countTrendText = countTrend > 0 ? `+${countTrend}` : countTrend;
+ const boardTrendText = boardTrend > 0 ? `+${boardTrend}` : boardTrend;
+ const countIcon = countTrend > 0 ? "🔥" : countTrend < 0 ? "❄️" : "➡️";
+
+ const historyText = (history || [])
+ .slice(0, 5)
+ .map((h) => `${h.date?.slice(5) || ""}: ${h.count}家/${h.maxBoard}板`)
+ .join("
");
+
+ return `
+
+ ${name} ${countIcon}
+
+
+ 涨停家数:
+ ${value[1]}家
+ (${countTrendText})
+
+
+ 最高连板:
+ ${value[0]}板
+ (${boardTrendText})
+
+
+
近5日趋势:
+
${historyText}
+
+ 点击查看详情
+ `;
+ },
+ },
+ legend: {
+ show: false,
+ },
+ grid: {
+ left: "10%",
+ right: "8%",
+ top: "12%",
+ bottom: "8%",
+ containLabel: true,
+ },
+ xAxis: {
+ type: "value",
+ name: "辨识度(最高板)",
+ nameLocation: "middle",
+ nameGap: 28,
+ nameTextStyle: { color: "rgba(255, 255, 255, 0.6)", fontSize: 13 },
+ min: 0,
+ max: maxX,
+ interval: 1,
+ axisLine: { lineStyle: { color: "rgba(255, 255, 255, 0.2)" } },
+ axisLabel: {
+ color: "rgba(255, 255, 255, 0.6)",
+ fontSize: 12,
+ formatter: "{value}板",
+ },
+ splitLine: { lineStyle: { color: "rgba(255, 255, 255, 0.05)" } },
+ },
+ yAxis: {
+ type: "value",
+ name: "热度(家数)",
+ nameLocation: "middle",
+ nameGap: 40,
+ nameTextStyle: { color: "rgba(255, 255, 255, 0.6)", fontSize: 13 },
+ min: 0,
+ max: maxY,
+ axisLine: { show: false },
+ axisLabel: { color: "rgba(255, 255, 255, 0.6)", fontSize: 12 },
+ splitLine: { lineStyle: { color: "rgba(255, 255, 255, 0.05)" } },
+ },
+ series,
+ };
+};
diff --git a/src/views/Community/components/ThemeCometChart/constants.js b/src/views/Community/components/ThemeCometChart/constants.js
new file mode 100644
index 00000000..6a67e317
--- /dev/null
+++ b/src/views/Community/components/ThemeCometChart/constants.js
@@ -0,0 +1,9 @@
+// ThemeCometChart 常量定义
+
+// 板块状态配置
+export const STATUS_CONFIG = {
+ rising: { name: "主升", color: "#FF4D4F" },
+ declining: { name: "退潮", color: "#52C41A" },
+ lurking: { name: "潜伏", color: "#1890FF" },
+ clustering: { name: "抱团", color: "#722ED1" },
+};
diff --git a/src/views/Community/components/ThemeCometChart/index.js b/src/views/Community/components/ThemeCometChart/index.js
new file mode 100644
index 00000000..1e60c45d
--- /dev/null
+++ b/src/views/Community/components/ThemeCometChart/index.js
@@ -0,0 +1,5 @@
+// ThemeCometChart 模块导出
+
+export { STATUS_CONFIG } from "./constants";
+export { generateChartOption } from "./chartOptions";
+export { default as ThemeDetailModal } from "./ThemeDetailModal";