From d15d637c4ed1027d96f95e40039c3961aff6172d Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 15 Jan 2026 11:44:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ThemeCometChart):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E3=80=81=E5=9B=BE=E8=A1=A8=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E3=80=81=E5=BC=B9=E7=AA=97=E5=88=B0=E5=AD=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - constants.js: STATUS_CONFIG 板块状态配置 - chartOptions.js: generateChartOption 图表配置生成 - ThemeDetailModal.js: 板块详情弹窗组件 - index.js: 模块统一导出 主文件从 ~400 行精简到 ~180 行 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Community/components/ThemeCometChart.js | 378 +++++------------- .../ThemeCometChart/ThemeDetailModal.js | 150 +++++++ .../ThemeCometChart/chartOptions.js | 143 +++++++ .../components/ThemeCometChart/constants.js | 9 + .../components/ThemeCometChart/index.js | 5 + 5 files changed, 396 insertions(+), 289 deletions(-) create mode 100644 src/views/Community/components/ThemeCometChart/ThemeDetailModal.js create mode 100644 src/views/Community/components/ThemeCometChart/chartOptions.js create mode 100644 src/views/Community/components/ThemeCometChart/constants.js create mode 100644 src/views/Community/components/ThemeCometChart/index.js 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";