From 26548c7036620ae4530b7b5e4697947aa002254f Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 18 Dec 2025 07:26:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0Company=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84UI=E4=B8=BAFUI=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/select-fix.css | 52 +++ .../components/CompanyHeader/index.tsx | 7 + .../panels/TradeDataPanel/KLineModule.tsx | 37 +- .../TradeDataPanel/MetricOverlaySearch.tsx | 350 ++++++++++++++++++ .../components/MarketDataView/types.ts | 12 + .../MarketDataView/utils/chartOptions.ts | 100 ++++- 6 files changed, 552 insertions(+), 6 deletions(-) create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/MetricOverlaySearch.tsx diff --git a/src/styles/select-fix.css b/src/styles/select-fix.css index be912986..0840878e 100644 --- a/src/styles/select-fix.css +++ b/src/styles/select-fix.css @@ -87,3 +87,55 @@ select::-webkit-scrollbar-thumb { select::-webkit-scrollbar-thumb:hover { background: #FFC107; } + +/** + * Ant Design AutoComplete 下拉框样式 (FUI 主题) + */ +.fui-autocomplete-dropdown { + background-color: #1a1a2e !important; + border: 1px solid rgba(212, 175, 55, 0.3) !important; + border-radius: 10px !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5) !important; +} + +.fui-autocomplete-dropdown .ant-select-item { + color: #ffffff !important; + padding: 10px 12px !important; + border-bottom: 1px solid rgba(212, 175, 55, 0.1); +} + +.fui-autocomplete-dropdown .ant-select-item:last-child { + border-bottom: none; +} + +.fui-autocomplete-dropdown .ant-select-item-option-active, +.fui-autocomplete-dropdown .ant-select-item:hover { + background-color: rgba(212, 175, 55, 0.15) !important; +} + +.fui-autocomplete-dropdown .ant-select-item-option-selected { + background-color: rgba(212, 175, 55, 0.25) !important; +} + +.fui-autocomplete-dropdown .ant-select-item-empty { + color: rgba(255, 255, 255, 0.5) !important; +} + +/* AutoComplete 下拉框滚动条 */ +.fui-autocomplete-dropdown::-webkit-scrollbar { + width: 6px; +} + +.fui-autocomplete-dropdown::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.fui-autocomplete-dropdown::-webkit-scrollbar-thumb { + background: rgba(212, 175, 55, 0.4); + border-radius: 3px; +} + +.fui-autocomplete-dropdown::-webkit-scrollbar-thumb:hover { + background: rgba(212, 175, 55, 0.6); +} diff --git a/src/views/Company/components/CompanyHeader/index.tsx b/src/views/Company/components/CompanyHeader/index.tsx index c82c947b..8f25815b 100644 --- a/src/views/Company/components/CompanyHeader/index.tsx +++ b/src/views/Company/components/CompanyHeader/index.tsx @@ -236,6 +236,7 @@ const SearchActions = memo<{ }} > : null} onKeyDown={handleKeyDown} /> diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx index 2b90d09d..bf0eb21d 100644 --- a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx @@ -1,7 +1,7 @@ // src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx -// K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标) +// K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标 + 商品数据叠加) -import React, { useState } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { Box, Text, @@ -33,7 +33,8 @@ import { type MainIndicatorType, type DrawingType, } from '../../../utils/chartOptions'; -import type { KLineModuleProps } from '../../../types'; +import type { KLineModuleProps, OverlayMetricData } from '../../../types'; +import MetricOverlaySearch from './MetricOverlaySearch'; // 空状态组件(内联) const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => ( @@ -95,8 +96,28 @@ const KLineModule: React.FC = ({ const [mainIndicator, setMainIndicator] = useState('MA'); const [showAnalysis, setShowAnalysis] = useState(true); const [drawingType, setDrawingType] = useState('NONE'); + const [overlayMetrics, setOverlayMetrics] = useState([]); const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0; + // 计算股票数据的日期范围(用于查询商品数据) + const stockDateRange = useMemo(() => { + if (tradeData.length === 0) return undefined; + return { + startDate: tradeData[0].date.substring(0, 10), + endDate: tradeData[tradeData.length - 1].date.substring(0, 10), + }; + }, [tradeData]); + + // 添加叠加指标 + const handleAddOverlayMetric = useCallback((metric: OverlayMetricData) => { + setOverlayMetrics(prev => [...prev, metric]); + }, []); + + // 移除叠加指标 + const handleRemoveOverlayMetric = useCallback((metricId: string) => { + setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId)); + }, []); + // 切换到分时模式时自动加载数据 const handleModeChange = (newMode: ChartMode) => { setMode(newMode); @@ -337,6 +358,14 @@ const KLineModule: React.FC = ({ ))} + + {/* 商品数据叠加搜索 */} + )} @@ -383,7 +412,7 @@ const KLineModule: React.FC = ({ tradeData.length > 0 ? ( void; + onRemoveMetric: (metricId: string) => void; + stockDateRange?: { startDate: string; endDate: string }; +} + +/** + * Metric overlay search component + */ +const MetricOverlaySearch: React.FC = ({ + overlayMetrics, + onAddMetric, + onRemoveMetric, + stockDateRange, +}) => { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [isLoadingData, setIsLoadingData] = useState(false); + const { isOpen, onOpen, onClose } = useDisclosure(); + const inputRef = useRef(null); + + // Get next available color + const getNextColor = useCallback(() => { + const usedColors = overlayMetrics.map(m => m.color); + const availableColor = OVERLAY_COLORS.find(c => !usedColors.includes(c)); + return availableColor || OVERLAY_COLORS[overlayMetrics.length % OVERLAY_COLORS.length]; + }, [overlayMetrics]); + + // Search metrics + const doSearch = useCallback(async (query: string) => { + if (!query.trim()) { + setSearchResults([]); + return; + } + + setIsSearching(true); + try { + const response = await searchMetrics(query, undefined, undefined, 20); + setSearchResults(response.results || []); + } catch (error) { + console.error('Search metrics failed:', error); + setSearchResults([]); + } finally { + setIsSearching(false); + } + }, []); + + // Debounced search + const debouncedSearch = useMemo( + () => debounce(doSearch, 300), + [doSearch] + ); + + // Handle input change + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchQuery(value); + debouncedSearch(value); + if (value && !isOpen) { + onOpen(); + } + }; + + // Select metric + const handleSelectMetric = async (metric: MetricSearchResult) => { + // Check if already added + if (overlayMetrics.some(m => m.metric_id === metric.metric_id)) { + return; + } + + // Max 3 overlays + if (overlayMetrics.length >= 3) { + alert('Max 3 overlay metrics allowed'); + return; + } + + setIsLoadingData(true); + try { + // Fetch metric data + const data = await fetchMetricData( + metric.metric_id, + stockDateRange?.startDate, + stockDateRange?.endDate, + 500 + ); + + if (data && data.data && data.data.length > 0) { + const overlayData: OverlayMetricData = { + metric_id: metric.metric_id, + metric_name: metric.metric_name, + source: metric.source, + unit: metric.unit, + data: data.data, + color: getNextColor(), + }; + + onAddMetric(overlayData); + setSearchQuery(''); + setSearchResults([]); + onClose(); + } else { + alert('No data available for this metric'); + } + } catch (error) { + console.error('Fetch metric data failed:', error); + alert('Failed to fetch metric data'); + } finally { + setIsLoadingData(false); + } + }; + + // Cleanup debounce + useEffect(() => { + return () => debouncedSearch.cancel?.(); + }, [debouncedSearch]); + + return ( + + {/* Added overlay metrics tags */} + {overlayMetrics.map((metric) => ( + + + + {metric.metric_name} + + onRemoveMetric(metric.metric_id)} /> + + ))} + + {/* Search button and dropdown */} + + + + + + + + + + + + {/* Search input */} + + + + + + {(searchQuery || isSearching) && ( + + {isSearching || isLoadingData ? ( + + ) : ( + } + onClick={() => { + setSearchQuery(''); + setSearchResults([]); + }} + aria-label="Clear" + _hover={{ bg: 'transparent' }} + /> + )} + + )} + + + {/* Search results */} + {searchResults.length > 0 ? ( + + {searchResults.map((metric) => { + const isAdded = overlayMetrics.some(m => m.metric_id === metric.metric_id); + return ( + !isAdded && handleSelectMetric(metric)} + borderRadius="md" + > + + + + {metric.metric_name} + + + + {metric.source} + + {metric.frequency && ( + + {metric.frequency} + + )} + {metric.unit && ( + + {metric.unit} + + )} + + + {isAdded ? ( + Added + ) : ( + + )} + + + ); + })} + + ) : searchQuery && !isSearching ? ( + + No metrics found + + ) : !searchQuery ? ( + + + + Enter keywords to search + + + Supports SMM and Mysteel data sources + + + ) : null} + + + + + ); +}; + +export default MetricOverlaySearch; diff --git a/src/views/Company/components/MarketDataView/types.ts b/src/views/Company/components/MarketDataView/types.ts index 0089f8bd..c3bb5eaf 100644 --- a/src/views/Company/components/MarketDataView/types.ts +++ b/src/views/Company/components/MarketDataView/types.ts @@ -351,6 +351,18 @@ export interface AnalysisModalContentProps { theme: Theme; } +/** + * 叠加指标数据 + */ +export interface OverlayMetricData { + metric_id: string; + metric_name: string; + source: string; + unit: string; + data: { date: string; value: number | null }[]; + color?: string; +} + /** * useMarketData Hook 返回值 */ diff --git a/src/views/Company/components/MarketDataView/utils/chartOptions.ts b/src/views/Company/components/MarketDataView/utils/chartOptions.ts index ae45e6bc..2ed81be8 100644 --- a/src/views/Company/components/MarketDataView/utils/chartOptions.ts +++ b/src/views/Company/components/MarketDataView/utils/chartOptions.ts @@ -9,6 +9,7 @@ import type { FundingDayData, PledgeData, RiseAnalysis, + OverlayMetricData, } from '../types'; import { formatNumber } from './formatUtils'; @@ -856,6 +857,7 @@ export const calculateTrendLine = ( * @param mainIndicator 主图指标 (默认MA) * @param showAnalysis 是否显示涨幅分析标记 (默认true) * @param drawingType 绘图工具类型 (默认NONE) + * @param overlayMetrics 叠加的商品指标数据 (可选) */ export const getKLineDarkGoldOption = ( tradeData: TradeDayData[], @@ -863,7 +865,8 @@ export const getKLineDarkGoldOption = ( subIndicator: IndicatorType = 'MACD', mainIndicator: MainIndicatorType = 'MA', showAnalysis: boolean = true, - drawingType: DrawingType = 'NONE' + drawingType: DrawingType = 'NONE', + overlayMetrics: OverlayMetricData[] = [] ): EChartsOption => { if (!tradeData || tradeData.length === 0) return {}; @@ -1034,6 +1037,65 @@ export const getKLineDarkGoldOption = ( }); } + // 处理叠加指标数据 - 使用右侧Y轴 + const hasOverlayMetrics = overlayMetrics && overlayMetrics.length > 0; + const overlaySeriesData: { name: string; data: (number | null)[]; color: string; unit: string }[] = []; + + if (hasOverlayMetrics) { + // 创建日期到索引的映射(股票数据日期) + const fullDates = tradeData.map((item) => item.date.substring(0, 10)); + const dateToIndex: Record = {}; + fullDates.forEach((date, idx) => { + dateToIndex[date] = idx; + }); + + // 为每个叠加指标处理数据 + overlayMetrics.forEach((metric) => { + const seriesData: (number | null)[] = new Array(tradeData.length).fill(null); + + metric.data.forEach((point) => { + if (point.value !== null && point.value !== undefined) { + const pointDate = point.date.substring(0, 10); + const idx = dateToIndex[pointDate]; + if (idx !== undefined) { + seriesData[idx] = point.value; + } + } + }); + + overlaySeriesData.push({ + name: metric.metric_name, + data: seriesData, + color: metric.color || '#00D4FF', + unit: metric.unit, + }); + }); + + // 为叠加指标添加右侧Y轴(只添加一个,所有叠加指标共用) + yAxes[0] = { + ...yAxes[0], + position: 'left', + }; + + // 右侧Y轴用于显示叠加指标 + const overlayYAxisIndex = hasSubIndicator ? 3 : 2; + yAxes.push({ + scale: true, + position: 'right', + axisLine: { lineStyle: { color: overlaySeriesData[0]?.color || '#00D4FF' } }, + axisLabel: { + color: overlaySeriesData[0]?.color || '#00D4FF', + fontSize: 10, + formatter: (value: number) => { + if (Math.abs(value) >= 10000) return (value / 10000).toFixed(1) + '万'; + if (Math.abs(value) >= 1000) return (value / 1000).toFixed(1) + 'k'; + return value.toFixed(1); + } + }, + splitLine: { show: false }, + }); + } + // 图例数据 const legendData = ['K线']; if (mainIndicator === 'MA') { @@ -1458,6 +1520,33 @@ export const getKLineDarkGoldOption = ( legendData.push('BIAS6', 'BIAS12', 'BIAS24'); } + // 添加叠加指标的 series(使用右侧Y轴) + if (hasOverlayMetrics && overlaySeriesData.length > 0) { + const overlayYAxisIndex = hasSubIndicator ? 3 : 2; + overlaySeriesData.forEach((overlayItem) => { + series.push({ + name: overlayItem.name, + type: 'line', + yAxisIndex: overlayYAxisIndex, + data: overlayItem.data, + smooth: true, + symbol: 'none', + lineStyle: { + color: overlayItem.color, + width: 2, + type: 'solid', + }, + emphasis: { + lineStyle: { + width: 3, + }, + }, + z: 90, // 确保在K线上方显示 + }); + legendData.push(overlayItem.name); + }); + } + return { backgroundColor: 'transparent', animation: true, @@ -1500,7 +1589,14 @@ export const getKLineDarkGoldOption = ( // 跳过涨幅分析标记 } else if (typeof value === 'number' || (value !== null && !isNaN(Number(value)))) { // MA、BOLL、MACD、KDJ、RSI 等指标 - result += `${marker} ${seriesName}:${formatPrice(Number(value))}
`; + // 检查是否是叠加指标 + const overlayItem = overlaySeriesData.find(o => o.name === seriesName); + if (overlayItem) { + // 叠加指标,显示单位 + result += `${marker} ${seriesName}:${formatPrice(Number(value))}${overlayItem.unit ? ' ' + overlayItem.unit : ''}
`; + } else { + result += `${marker} ${seriesName}:${formatPrice(Number(value))}
`; + } } });