From 5aa0507a6550e9f812ed4575d7aea89d98e86406 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 13 Nov 2025 16:07:14 +0800 Subject: [PATCH] update ui --- .../Community/components/CompactSearchBox.js | 610 ++++++++++++++++++ .../Community/components/DynamicNewsCard.js | 7 +- src/views/Community/components/HeroPanel.js | 463 +++++++++++++ src/views/Community/index.js | 15 +- 4 files changed, 1086 insertions(+), 9 deletions(-) create mode 100644 src/views/Community/components/CompactSearchBox.js create mode 100644 src/views/Community/components/HeroPanel.js diff --git a/src/views/Community/components/CompactSearchBox.js b/src/views/Community/components/CompactSearchBox.js new file mode 100644 index 00000000..34fec201 --- /dev/null +++ b/src/views/Community/components/CompactSearchBox.js @@ -0,0 +1,610 @@ +// src/views/Community/components/CompactSearchBox.js +// 紧凑版搜索和筛选组件 - 优化布局 + +import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; +import { + Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect, + Tooltip +} from 'antd'; +import { + SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined, + CalendarOutlined, SortAscendingOutlined +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import debounce from 'lodash/debounce'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice'; +import { stockService } from '../../../services/stockService'; +import { logger } from '../../../utils/logger'; +import TradingTimeFilter from './TradingTimeFilter'; + +const { Option } = AntSelect; + +const CompactSearchBox = ({ + onSearch, + onSearchFocus, + filters = {}, + mode, + pageSize, + trackingFunctions = {} +}) => { + // 状态 + const [stockOptions, setStockOptions] = useState([]); + const [allStocks, setAllStocks] = useState([]); + const [industryValue, setIndustryValue] = useState([]); + const [sort, setSort] = useState('new'); + const [importance, setImportance] = useState([]); + const [tradingTimeRange, setTradingTimeRange] = useState(null); + const [inputValue, setInputValue] = useState(''); + + // Redux + const dispatch = useDispatch(); + const industryData = useSelector(selectIndustryData); + const industryLoading = useSelector(selectIndustryLoading); + + // 防抖搜索 + const debouncedSearchRef = useRef(null); + + const triggerSearch = useCallback((params) => { + logger.debug('CompactSearchBox', '触发搜索', { params }); + onSearch(params); + }, [onSearch]); + + useEffect(() => { + debouncedSearchRef.current = debounce((params) => { + triggerSearch(params); + }, 300); + + return () => { + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } + }; + }, [triggerSearch]); + + // 加载股票数据 + useEffect(() => { + const loadStocks = async () => { + const response = await stockService.getAllStocks(); + if (response.success && response.data) { + setAllStocks(response.data); + } + }; + loadStocks(); + }, []); + + // 初始化筛选条件 + const findIndustryPath = useCallback((targetCode, data, currentPath = []) => { + if (!data || data.length === 0) return null; + + for (const item of data) { + const newPath = [...currentPath, item.value]; + + if (item.value === targetCode) { + return newPath; + } + + if (item.children && item.children.length > 0) { + const found = findIndustryPath(targetCode, item.children, newPath); + if (found) return found; + } + } + return null; + }, []); + + useEffect(() => { + if (!filters) return; + + if (filters.sort) setSort(filters.sort); + + if (filters.importance) { + const importanceArray = filters.importance === 'all' + ? [] + : filters.importance.split(',').map(v => v.trim()).filter(Boolean); + setImportance(importanceArray); + } else { + setImportance([]); + } + + if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) { + const path = findIndustryPath(filters.industry_code, industryData); + if (path) { + setIndustryValue(path); + } + } else if (!filters.industry_code && industryValue && industryValue.length > 0) { + setIndustryValue([]); + } + + if (filters.q) { + setInputValue(filters.q); + } else if (!filters.q) { + setInputValue(''); + } + + const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days; + + if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) { + let inferredKey = 'custom'; + let inferredLabel = ''; + + if (filters.recent_days) { + if (filters.recent_days === '7') { + inferredKey = 'week'; + inferredLabel = '近一周'; + } else if (filters.recent_days === '30') { + inferredKey = 'month'; + inferredLabel = '近一月'; + } else { + inferredLabel = `近${filters.recent_days}天`; + } + } else if (filters.start_date && filters.end_date) { + inferredLabel = `${dayjs(filters.start_date).format('MM-DD HH:mm')} - ${dayjs(filters.end_date).format('MM-DD HH:mm')}`; + } + + const timeRange = { + start_date: filters.start_date || '', + end_date: filters.end_date || '', + recent_days: filters.recent_days || '', + label: inferredLabel, + key: inferredKey + }; + setTradingTimeRange(timeRange); + } else if (!hasTimeInFilters && tradingTimeRange) { + setTradingTimeRange(null); + } + }, [filters, industryData, findIndustryPath, industryValue, tradingTimeRange]); + + // 搜索股票 + const handleSearch = (value) => { + if (!value || !allStocks || allStocks.length === 0) { + setStockOptions([]); + return; + } + + const results = stockService.fuzzySearch(value, allStocks, 10); + const options = results.map(stock => ({ + value: stock.code, + label: ( +
+ + {stock.code} + {stock.name} +
+ ), + stockInfo: stock + })); + + setStockOptions(options); + }; + + const buildFilterParams = useCallback((overrides = {}) => { + const sortValue = overrides.sort ?? sort; + let actualSort = sortValue; + let returnType; + + if (sortValue === 'returns_avg') { + actualSort = 'returns'; + returnType = 'avg'; + } else if (sortValue === 'returns_week') { + actualSort = 'returns'; + returnType = 'week'; + } + + let importanceValue = overrides.importance ?? importance; + if (Array.isArray(importanceValue)) { + importanceValue = importanceValue.length === 0 + ? 'all' + : importanceValue.join(','); + } + + const result = { + sort: actualSort, + importance: importanceValue, + q: (overrides.q ?? filters.q) ?? '', + industry_code: overrides.industry_code ?? (industryValue?.[industryValue.length - 1] || ''), + start_date: overrides.start_date ?? (tradingTimeRange?.start_date || ''), + end_date: overrides.end_date ?? (tradingTimeRange?.end_date || ''), + recent_days: overrides.recent_days ?? (tradingTimeRange?.recent_days || ''), + ...overrides, + page: 1, + }; + + delete result.per_page; + + if (returnType) { + result.return_type = returnType; + } + + if (mode !== undefined && mode !== null) { + result.mode = mode; + } + if (pageSize !== undefined && pageSize !== null) { + result.per_page = pageSize; + } + + return result; + }, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]); + + const handleStockSelect = (_value, option) => { + const stockInfo = option.stockInfo; + if (stockInfo) { + if (trackingFunctions.trackRelatedStockClicked) { + trackingFunctions.trackRelatedStockClicked({ + stockCode: stockInfo.code, + stockName: stockInfo.name, + source: 'search_box_autocomplete', + timestamp: new Date().toISOString(), + }); + } + + setInputValue(`${stockInfo.code} ${stockInfo.name}`); + + const params = buildFilterParams({ + q: stockInfo.code, + industry_code: '' + }); + triggerSearch(params); + } + }; + + const handleImportanceChange = (value) => { + setImportance(value); + + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } + + const importanceStr = value.length === 0 ? 'all' : value.join(','); + + if (trackingFunctions.trackNewsFilterApplied) { + trackingFunctions.trackNewsFilterApplied({ + filterType: 'importance', + filterValue: importanceStr, + timestamp: new Date().toISOString(), + }); + } + + const params = buildFilterParams({ importance: importanceStr }); + triggerSearch(params); + }; + + const handleSortChange = (value) => { + setSort(value); + + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } + + if (trackingFunctions.trackNewsSorted) { + trackingFunctions.trackNewsSorted({ + sortBy: value, + previousSortBy: sort, + timestamp: new Date().toISOString(), + }); + } + + const params = buildFilterParams({ sort: value }); + triggerSearch(params); + }; + + const handleIndustryChange = (value) => { + setIndustryValue(value); + + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } + + if (trackingFunctions.trackNewsFilterApplied) { + trackingFunctions.trackNewsFilterApplied({ + filterType: 'industry', + filterValue: value?.[value.length - 1] || '', + timestamp: new Date().toISOString(), + }); + } + + const params = buildFilterParams({ + industry_code: value?.[value.length - 1] || '' + }); + triggerSearch(params); + }; + + const handleTradingTimeChange = (timeConfig) => { + if (!timeConfig) { + setTradingTimeRange(null); + + if (trackingFunctions.trackNewsFilterApplied) { + trackingFunctions.trackNewsFilterApplied({ + filterType: 'time_range', + filterValue: 'cleared', + timestamp: new Date().toISOString(), + }); + } + + const params = buildFilterParams({ + start_date: '', + end_date: '', + recent_days: '' + }); + triggerSearch(params); + return; + } + + const { range, type, label, key } = timeConfig; + let params = {}; + + if (type === 'recent_days') { + params.recent_days = range; + params.start_date = ''; + params.end_date = ''; + } else { + params.start_date = range[0].format('YYYY-MM-DD HH:mm:ss'); + params.end_date = range[1].format('YYYY-MM-DD HH:mm:ss'); + params.recent_days = ''; + } + + setTradingTimeRange({ ...params, label, key }); + + if (trackingFunctions.trackNewsFilterApplied) { + trackingFunctions.trackNewsFilterApplied({ + filterType: 'time_range', + filterValue: label, + timeRangeType: type, + timestamp: new Date().toISOString(), + }); + } + + const searchParams = buildFilterParams({ ...params, mode }); + triggerSearch(searchParams); + }; + + const handleMainSearch = () => { + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } + + const params = buildFilterParams({ + q: inputValue, + industry_code: '' + }); + + if (trackingFunctions.trackNewsSearched && inputValue) { + trackingFunctions.trackNewsSearched({ + searchQuery: inputValue, + searchType: 'main_search', + filters: params, + timestamp: new Date().toISOString(), + }); + } + + triggerSearch(params); + }; + + const handleInputChange = (value) => { + setInputValue(value); + }; + + const handleReset = () => { + setInputValue(''); + setStockOptions([]); + setIndustryValue([]); + setSort('new'); + setImportance([]); + setTradingTimeRange(null); + + if (trackingFunctions.trackNewsFilterApplied) { + trackingFunctions.trackNewsFilterApplied({ + filterType: 'reset', + filterValue: 'all_filters_cleared', + timestamp: new Date().toISOString(), + }); + } + + const resetParams = { + q: '', + industry_code: '', + sort: 'new', + importance: 'all', + start_date: '', + end_date: '', + recent_days: '', + page: 1, + _forceRefresh: Date.now() + }; + + onSearch(resetParams); + }; + + const handleCascaderFocus = async () => { + if (!industryData || industryData.length === 0) { + dispatch(fetchIndustryData()); + } + }; + + return ( +
+ {/* 单行紧凑布局 */} + + {/* 搜索框 */} + { + if (e.key === 'Enter') { + handleMainSearch(); + } + }} + style={{ width: 220 }} + size="small" + > + } + size="small" + /> + + + {/* 时间筛选 */} + +
+ + +
+
+ + {/* 行业筛选 */} + + + path.some(option => + option.label.toLowerCase().includes(inputValue.toLowerCase()) + ) + }} + allowClear + expandTrigger="hover" + displayRender={(labels) => labels[labels.length - 1] || '行业'} + disabled={industryLoading} + style={{ width: 100 }} + size="small" + suffixIcon={} + /> + + + {/* 重要性筛选 */} + + `已选 ${omittedValues.length} 项`} + > + + + + + + + + {/* 排序 */} + + } + > + + + + + + + + + {/* 重置按钮 */} + + + +
+ + {/* 激活的筛选标签(如果有的话) */} + {(inputValue || industryValue.length > 0 || importance.length > 0 || tradingTimeRange || sort !== 'new') && ( +
+ {inputValue && ( + { + setInputValue(''); + const params = buildFilterParams({ q: '' }); + triggerSearch(params); + }} color="blue"> + 搜索: {inputValue} + + )} + {tradingTimeRange && ( + { + setTradingTimeRange(null); + const params = buildFilterParams({ + start_date: '', + end_date: '', + recent_days: '' + }); + triggerSearch(params); + }} color="green"> + {tradingTimeRange.label} + + )} + {industryValue.length > 0 && industryData && ( + { + setIndustryValue([]); + const params = buildFilterParams({ industry_code: '' }); + triggerSearch(params); + }} color="orange"> + 行业: {(() => { + const findLabel = (code, data) => { + for (const item of data) { + if (code.startsWith(item.value)) { + if (item.value === code) { + return item.label; + } else { + return findLabel(code, item.children); + } + } + } + return null; + }; + const lastLevelCode = industryValue[industryValue.length - 1]; + return findLabel(lastLevelCode, industryData); + })()} + + )} + {importance.length > 0 && ( + { + setImportance([]); + const params = buildFilterParams({ importance: 'all' }); + triggerSearch(params); + }} color="purple"> + 重要性: {importance.map(imp => ({ 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }[imp] || imp)).join(', ')} + + )} + {sort && sort !== 'new' && ( + { + setSort('new'); + const params = buildFilterParams({ sort: 'new' }); + triggerSearch(params); + }} color="cyan"> + 排序: {({ 'hot': '最热', 'importance': '重要性', 'returns_avg': '平均收益', 'returns_week': '周收益' }[sort] || sort)} + + )} +
+ )} +
+ ); +}; + +export default CompactSearchBox; diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index 2afb12c1..116e0fca 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -35,7 +35,7 @@ import EventScrollList from './DynamicNewsCard/EventScrollList'; import ModeToggleButtons from './DynamicNewsCard/ModeToggleButtons'; import PaginationControl from './DynamicNewsCard/PaginationControl'; import DynamicNewsDetailPanel from './DynamicNewsDetail'; -import UnifiedSearchBox from './UnifiedSearchBox'; +import CompactSearchBox from './CompactSearchBox'; import { fetchDynamicNews, toggleEventFollow, @@ -597,12 +597,11 @@ const [currentMode, setCurrentMode] = useState('vertical'); - {/* 搜索和筛选组件 */} + {/* 搜索和筛选组件 - 紧凑版 */} - { + try { + const response = await fetch(`/api/index/${indexCode}/kline`); + const data = await response.json(); + return data; + } catch (error) { + logger.error('HeroPanel', 'fetchIndexKline', error); + return null; + } +}; + +/** + * 获取热门概念数据(用于词云图) + */ +const fetchPopularConcepts = async () => { + try { + const response = await fetch('/concept-api/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: '', + size: 50, // 获取前50个概念用于词云 + page: 1, + sort_by: 'change_pct' + }) + }); + + const data = await response.json(); + if (data.results) { + return data.results.map(item => ({ + name: item.concept, + value: Math.abs(item.price_info?.avg_change_pct || 1), // 使用涨跌幅绝对值作为权重 + change_pct: item.price_info?.avg_change_pct || 0, + })); + } + return []; + } catch (error) { + logger.error('HeroPanel', 'fetchPopularConcepts', error); + return []; + } +}; + +/** + * 迷你折线图组件 + */ +const MiniIndexChart = ({ indexCode, indexName }) => { + const [chartData, setChartData] = useState(null); + const [loading, setLoading] = useState(true); + const [latestData, setLatestData] = useState(null); + + const chartBg = useColorModeValue('transparent', 'transparent'); + const lineColor = useColorModeValue('#FFD700', '#FFD700'); // 金色 + const areaColor = useColorModeValue('rgba(255, 215, 0, 0.15)', 'rgba(255, 215, 0, 0.1)'); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + const data = await fetchIndexKline(indexCode); + + if (data && data.data && data.data.length > 0) { + // 取最近一个交易日的数据 + const latest = data.data[data.data.length - 1]; + setLatestData({ + close: latest[2], + change: ((latest[2] - latest[1]) / latest[1] * 100).toFixed(2), + isPositive: latest[2] >= latest[1] + }); + + // 准备图表数据(最近60个交易日) + const recentData = data.data.slice(-60); + setChartData({ + dates: recentData.map(item => item[0]), + values: recentData.map(item => item[2]) + }); + } + setLoading(false); + }; + + loadData(); + }, [indexCode]); + + const chartOption = useMemo(() => { + if (!chartData) return {}; + + return { + backgroundColor: chartBg, + grid: { + left: 5, + right: 5, + top: 5, + bottom: 20, + containLabel: false + }, + xAxis: { + type: 'category', + data: chartData.dates, + show: false + }, + yAxis: { + type: 'value', + show: false, + scale: true + }, + series: [{ + type: 'line', + data: chartData.values, + smooth: true, + symbol: 'none', + lineStyle: { + color: lineColor, + width: 2, + shadowColor: lineColor, + shadowBlur: 8, + shadowOffsetY: 2 + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, + color: areaColor + }, { + offset: 1, + color: 'rgba(255, 215, 0, 0)' + }] + } + } + }] + }; + }, [chartData, chartBg, lineColor, areaColor]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + + + + {indexName} + + {latestData?.close.toFixed(2)} + + + + + + {latestData?.isPositive ? '+' : ''}{latestData?.change}% + + + + + + + + ); +}; + +/** + * 概念词云图组件 + */ +const ConceptWordCloud = () => { + const [concepts, setConcepts] = useState([]); + const [loading, setLoading] = useState(true); + + const chartBg = useColorModeValue('transparent', 'transparent'); + + useEffect(() => { + const loadConcepts = async () => { + setLoading(true); + const data = await fetchPopularConcepts(); + setConcepts(data); + setLoading(false); + }; + + loadConcepts(); + }, []); + + const chartOption = useMemo(() => { + if (concepts.length === 0) return {}; + + return { + backgroundColor: chartBg, + tooltip: { + show: true, + formatter: (params) => { + const changePct = params.data.change_pct; + const sign = changePct > 0 ? '+' : ''; + return `${params.name}
涨跌: ${sign}${changePct.toFixed(2)}%`; + } + }, + series: [{ + type: 'wordCloud', + shape: 'circle', + left: 'center', + top: 'center', + width: '100%', + height: '100%', + right: null, + bottom: null, + sizeRange: [12, 40], + rotationRange: [-45, 45], + rotationStep: 45, + gridSize: 8, + drawOutOfBound: false, + layoutAnimation: true, + textStyle: { + fontFamily: 'sans-serif', + fontWeight: 'bold', + color: function (params) { + // 根据涨跌幅设置颜色 + const changePct = params.data.change_pct; + if (changePct > 5) return '#ff4d4f'; // 大涨:红色 + if (changePct > 3) return '#ff7875'; // 中涨:浅红 + if (changePct > 0) return '#ffa940'; // 小涨:橙色 + if (changePct === 0) return '#FFD700'; // 平盘:金色 + if (changePct > -3) return '#95de64'; // 小跌:浅绿 + if (changePct > -5) return '#52c41a'; // 中跌:绿色 + return '#13c2c2'; // 大跌:青色 + } + }, + emphasis: { + focus: 'self', + textStyle: { + shadowBlur: 10, + shadowColor: '#FFD700', + fontSize: 20 + } + }, + data: concepts + }] + }; + }, [concepts, chartBg]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + + ); +}; + +/** + * 产品特性图标组件 + */ +const FeatureIcon = ({ icon, title, description }) => { + return ( + + + + + + + {title} + + + {description} + + + + ); +}; + +/** + * 顶部说明面板主组件 + */ +const HeroPanel = () => { + const gradientBg = 'linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%)'; + const cardBorder = '1px solid'; + const borderColor = useColorModeValue('rgba(255, 215, 0, 0.3)', 'rgba(255, 215, 0, 0.2)'); + + return ( + + {/* 装饰性背景图案 */} + + + + + {/* 左侧:产品介绍 */} + + + + + 价值前沿 + + + + 实时捕捉市场动态,智能分析投资机会。 +
+ 整合多维数据源,为您提供专业的投资决策支持。 +
+ + + + + + + +
+
+ + {/* 中间:沪深指数折线图 */} + + + + + + + + + + + + {/* 右侧:热门概念词云图 */} + + + + + 🔥 热门概念 + + + 实时更新 + + + + + + + 字体大小表示热度 · 颜色表示涨跌幅 + + + +
+
+
+ ); +}; + +export default HeroPanel; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index ff0a43c0..88ad6a92 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -24,6 +24,7 @@ import { // 导入组件 import DynamicNewsCard from './components/DynamicNewsCard'; import HotEventsSection from './components/HotEventsSection'; +import HeroPanel from './components/HeroPanel'; // 导入自定义 Hooks import { useEventData } from './hooks/useEventData'; @@ -175,11 +176,9 @@ const Community = () => { )} - {/* 热点事件区域 */} - + + {/* 顶部说明面板:产品介绍 + 沪深指数 + 热门概念词云 */} + {/* 实时要闻·动态追踪 - 横向滚动 */} { trackRelatedStockClicked: communityEvents.trackRelatedStockClicked, }} /> + + {/* 热点事件区域 - 移至底部 */} +
);