diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js
index 11828a35..0758544d 100644
--- a/src/views/Community/components/HeroPanel.js
+++ b/src/views/Community/components/HeroPanel.js
@@ -57,6 +57,7 @@ import ReactMarkdown from 'react-markdown';
import dayjs from 'dayjs';
import KLineChartModal from '@components/StockChart/KLineChartModal';
import { FullCalendarPro } from '@components/Calendar';
+import ThemeCometChart from './ThemeCometChart';
const { TabPane } = Tabs;
const { Text: AntText } = Typography;
@@ -2708,15 +2709,30 @@ const HeroPanel = () => {
- {/* 综合日历区域 - 左侧1/3空白,右侧2/3日历 */}
+ {/* AI舆情时空决策驾驶舱 - 左侧词频流星图,右侧日历 */}
- {/* 左侧留白区域 - 占 1/3 */}
+ {/* 左侧:词频流星图 - 占 1/2 */}
- {/* 预留空间供后续使用 */}
+ {
+ // 当用户点击某个主题时,可以展示相关股票
+ if (data?.stocks?.length > 0) {
+ setSelectedSectorInfo({
+ name: data.theme.label,
+ count: data.stocks.length,
+ stocks: data.stocks.map((s) => ({
+ ...s,
+ _continuousDays: parseInt(s.continuous_days) || 1,
+ })),
+ });
+ setSectorStocksModalVisible(true);
+ }
+ }}
+ />
- {/* 右侧日历区域 - 占 2/3 */}
-
+ {/* 右侧:综合日历 - 占 1/2 */}
+
diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js
new file mode 100644
index 00000000..c09d2f53
--- /dev/null
+++ b/src/views/Community/components/ThemeCometChart.js
@@ -0,0 +1,456 @@
+/**
+ * ThemeCometChart - 词频流星图
+ * 展示核心主题词频的时间演变轨迹,用流星拖尾效果展示趋势
+ */
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import {
+ Box,
+ Text,
+ VStack,
+ HStack,
+ Spinner,
+ Center,
+ Badge,
+ Tooltip,
+ useToast,
+} from '@chakra-ui/react';
+import ReactECharts from 'echarts-for-react';
+import { RocketOutlined } from '@ant-design/icons';
+
+// 核心主题词配置(过滤噪音词,保留有意义的主题)
+const CORE_THEMES = [
+ { word: '航天', label: '商业航天', color: '#FF6B6B' },
+ { word: '机器人', label: '机器人', color: '#4ECDC4' },
+ { word: '脑机', label: '脑机接口', color: '#A78BFA' },
+ { word: '光刻胶', label: '光刻胶', color: '#F59E0B' },
+ { word: '算力', label: 'AI算力', color: '#3B82F6' },
+ { word: '电池', label: '固态电池', color: '#10B981' },
+ { word: '液冷', label: 'AI液冷', color: '#06B6D4' },
+ { word: '军工', label: '军工', color: '#EF4444' },
+];
+
+// 噪音词黑名单
+const NOISE_WORDS = [
+ '落地', '科技', '智能', '标的', '资产', '传闻', '业绩', '估值',
+ '商业', '卫星', '国产', '替代', '材料', '概念', '板块', '公司',
+ '市场', '预期', '政策', '产业', '技术', '研发', '布局', '龙头',
+];
+
+/**
+ * 从 public/data/zt 读取多日词频数据
+ */
+const fetchMultiDayWordFreq = async (days = 7) => {
+ try {
+ // 先获取日期列表
+ const datesRes = await fetch('/data/zt/dates.json');
+ const datesData = await datesRes.json();
+ const recentDates = datesData.dates.slice(0, days);
+
+ // 并行获取每日数据
+ const dailyDataPromises = recentDates.map(async (dateInfo) => {
+ try {
+ const res = await fetch(`/data/zt/daily/${dateInfo.date}.json`);
+ if (!res.ok) return null;
+ const data = await res.json();
+ return {
+ date: dateInfo.date,
+ formattedDate: dateInfo.formatted_date,
+ wordFreq: data.word_freq_data || [],
+ sectorData: data.sector_data || {},
+ stocks: data.stocks || [],
+ totalStocks: dateInfo.count,
+ };
+ } catch {
+ return null;
+ }
+ });
+
+ const results = await Promise.all(dailyDataPromises);
+ return results.filter(Boolean).reverse(); // 按时间正序排列
+ } catch (error) {
+ console.error('获取词频数据失败:', error);
+ return [];
+ }
+};
+
+/**
+ * 处理词频数据,提取核心主题的时间序列
+ */
+const processWordFreqData = (dailyData) => {
+ if (!dailyData || dailyData.length === 0) return { themes: [], dates: [] };
+
+ const dates = dailyData.map((d) => d.formattedDate.slice(5)); // MM-DD 格式
+ const themes = [];
+
+ CORE_THEMES.forEach((theme) => {
+ const values = dailyData.map((day) => {
+ // 查找匹配的词频
+ const found = day.wordFreq.find((w) => w.name.includes(theme.word));
+ return found ? found.value : 0;
+ });
+
+ // 归一化到 0-100
+ const maxVal = Math.max(...values, 1);
+ const normalizedValues = values.map((v) => Math.round((v / maxVal) * 100));
+
+ // 计算趋势(最近两天的变化)
+ const trend =
+ normalizedValues.length >= 2
+ ? normalizedValues[normalizedValues.length - 1] -
+ normalizedValues[normalizedValues.length - 2]
+ : 0;
+
+ themes.push({
+ ...theme,
+ values: normalizedValues,
+ rawValues: values,
+ trend,
+ currentValue: normalizedValues[normalizedValues.length - 1] || 0,
+ });
+ });
+
+ // 按当前热度排序
+ themes.sort((a, b) => b.currentValue - a.currentValue);
+
+ return { themes, dates, dailyData };
+};
+
+/**
+ * 生成 ECharts 配置
+ */
+const generateChartOption = (themes, dates, onThemeClick) => {
+ if (!themes || themes.length === 0) {
+ return {};
+ }
+
+ // 只显示 Top 6 主题
+ const topThemes = themes.slice(0, 6);
+
+ const series = [];
+
+ topThemes.forEach((theme, index) => {
+ // 主线条 - 带渐变的折线
+ series.push({
+ name: theme.label,
+ type: 'line',
+ smooth: true,
+ symbol: 'none',
+ lineStyle: {
+ width: 3,
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 1,
+ y2: 0,
+ colorStops: [
+ { offset: 0, color: `${theme.color}33` }, // 起点透明
+ { offset: 0.5, color: `${theme.color}88` },
+ { offset: 1, color: theme.color }, // 终点实色
+ ],
+ },
+ shadowColor: theme.color,
+ shadowBlur: 10,
+ },
+ areaStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 0, color: `${theme.color}40` },
+ { offset: 1, color: `${theme.color}05` },
+ ],
+ },
+ },
+ data: theme.values,
+ z: 10 + index,
+ });
+
+ // 流星头 - 只在最后一个点显示
+ const lastValue = theme.values[theme.values.length - 1];
+ const effectData = new Array(theme.values.length - 1).fill(null);
+ effectData.push(lastValue);
+
+ series.push({
+ name: `${theme.label}-head`,
+ type: 'effectScatter',
+ coordinateSystem: 'cartesian2d',
+ data: effectData.map((v, i) => (v !== null ? [i, v] : null)).filter(Boolean),
+ symbolSize: (val) => Math.max(12, val[1] / 5),
+ showEffectOn: 'render',
+ rippleEffect: {
+ brushType: 'stroke',
+ scale: 3,
+ period: 4,
+ },
+ itemStyle: {
+ color: theme.color,
+ shadowBlur: 20,
+ shadowColor: theme.color,
+ },
+ label: {
+ show: true,
+ formatter: theme.label,
+ position: 'right',
+ color: theme.color,
+ fontSize: 12,
+ fontWeight: 'bold',
+ textShadowColor: 'rgba(0,0,0,0.8)',
+ textShadowBlur: 4,
+ },
+ z: 20 + index,
+ });
+ });
+
+ return {
+ backgroundColor: 'transparent',
+ tooltip: {
+ trigger: 'axis',
+ backgroundColor: 'rgba(15, 15, 30, 0.95)',
+ borderColor: 'rgba(255, 215, 0, 0.3)',
+ borderWidth: 1,
+ textStyle: {
+ color: '#fff',
+ },
+ formatter: (params) => {
+ if (!params || params.length === 0) return '';
+ const dateIndex = params[0].dataIndex;
+ const date = dates[dateIndex];
+ let html = `
${date}
`;
+ params
+ .filter((p) => p.seriesName && !p.seriesName.includes('-head'))
+ .forEach((p) => {
+ const theme = topThemes.find((t) => t.label === p.seriesName);
+ if (theme) {
+ const trend = p.dataIndex > 0 ? theme.values[p.dataIndex] - theme.values[p.dataIndex - 1] : 0;
+ const trendIcon = trend > 0 ? '🔥' : trend < 0 ? '❄️' : '➡️';
+ html += `
+ ${p.seriesName}
+ ${p.value} ${trendIcon}
+
`;
+ }
+ });
+ return html;
+ },
+ },
+ legend: {
+ show: false,
+ },
+ grid: {
+ left: '3%',
+ right: '15%',
+ top: '10%',
+ bottom: '12%',
+ containLabel: true,
+ },
+ xAxis: {
+ type: 'category',
+ data: dates,
+ boundaryGap: false,
+ axisLine: {
+ lineStyle: {
+ color: 'rgba(255, 255, 255, 0.2)',
+ },
+ },
+ axisLabel: {
+ color: 'rgba(255, 255, 255, 0.6)',
+ fontSize: 11,
+ },
+ splitLine: {
+ show: false,
+ },
+ },
+ yAxis: {
+ type: 'value',
+ min: 0,
+ max: 100,
+ axisLine: {
+ show: false,
+ },
+ axisLabel: {
+ color: 'rgba(255, 255, 255, 0.4)',
+ fontSize: 10,
+ formatter: '{value}',
+ },
+ splitLine: {
+ lineStyle: {
+ color: 'rgba(255, 255, 255, 0.05)',
+ },
+ },
+ },
+ series,
+ };
+};
+
+/**
+ * ThemeCometChart 主组件
+ */
+const ThemeCometChart = ({ onThemeSelect }) => {
+ const [loading, setLoading] = useState(true);
+ const [data, setData] = useState({ themes: [], dates: [], dailyData: [] });
+ const [selectedTheme, setSelectedTheme] = useState(null);
+ const toast = useToast();
+
+ // 加载数据
+ useEffect(() => {
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ const dailyData = await fetchMultiDayWordFreq(7);
+ const processed = processWordFreqData(dailyData);
+ setData(processed);
+ } catch (error) {
+ console.error('加载词频数据失败:', error);
+ toast({
+ title: '加载数据失败',
+ status: 'error',
+ duration: 3000,
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadData();
+ }, [toast]);
+
+ // 图表配置
+ const chartOption = useMemo(() => {
+ return generateChartOption(data.themes, data.dates, setSelectedTheme);
+ }, [data]);
+
+ // 点击主题
+ const handleThemeClick = useCallback(
+ (theme) => {
+ setSelectedTheme(theme);
+ if (onThemeSelect) {
+ // 找出该主题相关的股票
+ const latestDay = data.dailyData[data.dailyData.length - 1];
+ if (latestDay) {
+ const relatedStocks = latestDay.stocks.filter(
+ (stock) =>
+ stock.brief?.includes(theme.word) ||
+ stock.core_sectors?.some((s) => s.includes(theme.word))
+ );
+ onThemeSelect({
+ theme,
+ stocks: relatedStocks,
+ date: latestDay.formattedDate,
+ });
+ }
+ }
+ },
+ [data, onThemeSelect]
+ );
+
+ // 图表事件
+ const onChartEvents = useMemo(
+ () => ({
+ click: (params) => {
+ if (params.seriesName && !params.seriesName.includes('-head')) {
+ const theme = data.themes.find((t) => t.label === params.seriesName);
+ if (theme) {
+ handleThemeClick(theme);
+ }
+ }
+ },
+ }),
+ [data.themes, handleThemeClick]
+ );
+
+ if (loading) {
+ return (
+
+
+
+ 加载词频数据...
+
+
+ );
+ }
+
+ if (!data.themes || data.themes.length === 0) {
+ return (
+
+ 暂无词频数据
+
+ );
+ }
+
+ return (
+
+ {/* 标题栏 */}
+
+
+
+
+
+
+ 题材热力追踪
+
+
+ 核心主题词频演变 · 最近7个交易日
+
+
+
+
+ {/* 图表区域 */}
+
+
+
+
+ {/* 底部图例 */}
+
+ {data.themes.slice(0, 6).map((theme) => (
+ 0 ? '+' : ''}${theme.trend}`}
+ placement="top"
+ >
+ handleThemeClick(theme)}
+ _hover={{
+ bg: `${theme.color}40`,
+ transform: 'scale(1.05)',
+ }}
+ transition="all 0.2s"
+ >
+ {theme.label}
+ {theme.trend > 5 && ' 🔥'}
+ {theme.trend < -5 && ' ❄️'}
+
+
+ ))}
+
+
+ );
+};
+
+export default ThemeCometChart;