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 */}
+
+
+
+
+ }
+ onClick={onOpen}
+ color={darkGoldTheme.textMuted}
+ borderColor={darkGoldTheme.border}
+ _hover={{
+ bg: 'rgba(212, 175, 55, 0.1)',
+ borderColor: darkGoldTheme.gold,
+ color: darkGoldTheme.gold,
+ }}
+ minW="100px"
+ >
+ Data Search
+
+
+
+
+
+
+
+ {/* 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))}
`;
+ }
}
});