更新Company页面的UI为FUI风格
This commit is contained in:
@@ -87,3 +87,55 @@ select::-webkit-scrollbar-thumb {
|
|||||||
select::-webkit-scrollbar-thumb:hover {
|
select::-webkit-scrollbar-thumb:hover {
|
||||||
background: #FFC107;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ const SearchActions = memo<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
|
popupClassName="fui-autocomplete-dropdown"
|
||||||
value={inputCode}
|
value={inputCode}
|
||||||
options={stockOptions}
|
options={stockOptions}
|
||||||
onSearch={doSearch}
|
onSearch={doSearch}
|
||||||
@@ -243,6 +244,12 @@ const SearchActions = memo<{
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
placeholder="输入代码、名称或拼音"
|
placeholder="输入代码、名称或拼音"
|
||||||
style={{ width: 240 }}
|
style={{ width: 240 }}
|
||||||
|
dropdownStyle={{
|
||||||
|
backgroundColor: FUI_COLORS.bg.elevated,
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: `1px solid ${FUI_COLORS.line.emphasis}`,
|
||||||
|
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
|
||||||
|
}}
|
||||||
notFoundContent={isSearching ? <Spin size="small" /> : null}
|
notFoundContent={isSearching ? <Spin size="small" /> : null}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx
|
// 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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -33,7 +33,8 @@ import {
|
|||||||
type MainIndicatorType,
|
type MainIndicatorType,
|
||||||
type DrawingType,
|
type DrawingType,
|
||||||
} from '../../../utils/chartOptions';
|
} 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 }) => (
|
const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => (
|
||||||
@@ -95,8 +96,28 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
|||||||
const [mainIndicator, setMainIndicator] = useState<MainIndicatorType>('MA');
|
const [mainIndicator, setMainIndicator] = useState<MainIndicatorType>('MA');
|
||||||
const [showAnalysis, setShowAnalysis] = useState<boolean>(true);
|
const [showAnalysis, setShowAnalysis] = useState<boolean>(true);
|
||||||
const [drawingType, setDrawingType] = useState<DrawingType>('NONE');
|
const [drawingType, setDrawingType] = useState<DrawingType>('NONE');
|
||||||
|
const [overlayMetrics, setOverlayMetrics] = useState<OverlayMetricData[]>([]);
|
||||||
const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0;
|
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) => {
|
const handleModeChange = (newMode: ChartMode) => {
|
||||||
setMode(newMode);
|
setMode(newMode);
|
||||||
@@ -337,6 +358,14 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
|||||||
))}
|
))}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
{/* 商品数据叠加搜索 */}
|
||||||
|
<MetricOverlaySearch
|
||||||
|
overlayMetrics={overlayMetrics}
|
||||||
|
onAddMetric={handleAddOverlayMetric}
|
||||||
|
onRemoveMetric={handleRemoveOverlayMetric}
|
||||||
|
stockDateRange={stockDateRange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -383,7 +412,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
|||||||
tradeData.length > 0 ? (
|
tradeData.length > 0 ? (
|
||||||
<Box h="650px">
|
<Box h="650px">
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={getKLineDarkGoldOption(tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType)}
|
option={getKLineDarkGoldOption(tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType, overlayMetrics)}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
notMerge={true}
|
notMerge={true}
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/MetricOverlaySearch.tsx
|
||||||
|
// Metric overlay search component - search metrics and overlay on K-line chart
|
||||||
|
|
||||||
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
InputRightElement,
|
||||||
|
IconButton,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagCloseButton,
|
||||||
|
Spinner,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverBody,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
Flex,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { SearchIcon, CloseIcon, AddIcon } from '@chakra-ui/icons';
|
||||||
|
import { Database, TrendingUp } from 'lucide-react';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
import { searchMetrics, fetchMetricData, type MetricSearchResult } from '@services/categoryService';
|
||||||
|
import { darkGoldTheme } from '../../../constants';
|
||||||
|
import type { OverlayMetricData } from '../../../types';
|
||||||
|
|
||||||
|
// Available overlay colors
|
||||||
|
const OVERLAY_COLORS = [
|
||||||
|
'#00D4FF', // Cyan
|
||||||
|
'#A855F7', // Purple
|
||||||
|
'#FF6B6B', // Pink-red
|
||||||
|
'#4ADE80', // Green
|
||||||
|
'#F97316', // Orange
|
||||||
|
'#EC4899', // Pink
|
||||||
|
];
|
||||||
|
|
||||||
|
interface MetricOverlaySearchProps {
|
||||||
|
overlayMetrics: OverlayMetricData[];
|
||||||
|
onAddMetric: (metric: OverlayMetricData) => void;
|
||||||
|
onRemoveMetric: (metricId: string) => void;
|
||||||
|
stockDateRange?: { startDate: string; endDate: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric overlay search component
|
||||||
|
*/
|
||||||
|
const MetricOverlaySearch: React.FC<MetricOverlaySearchProps> = ({
|
||||||
|
overlayMetrics,
|
||||||
|
onAddMetric,
|
||||||
|
onRemoveMetric,
|
||||||
|
stockDateRange,
|
||||||
|
}) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [searchResults, setSearchResults] = useState<MetricSearchResult[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<HStack spacing={2} align="center">
|
||||||
|
{/* Added overlay metrics tags */}
|
||||||
|
{overlayMetrics.map((metric) => (
|
||||||
|
<Tag
|
||||||
|
key={metric.metric_id}
|
||||||
|
size="sm"
|
||||||
|
borderRadius="full"
|
||||||
|
variant="outline"
|
||||||
|
borderColor={metric.color}
|
||||||
|
color={metric.color}
|
||||||
|
bg="transparent"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w="8px"
|
||||||
|
h="8px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg={metric.color}
|
||||||
|
mr={1}
|
||||||
|
/>
|
||||||
|
<TagLabel fontSize="xs" maxW="100px" isTruncated>
|
||||||
|
{metric.metric_name}
|
||||||
|
</TagLabel>
|
||||||
|
<TagCloseButton onClick={() => onRemoveMetric(metric.metric_id)} />
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Search button and dropdown */}
|
||||||
|
<Popover
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
placement="bottom-start"
|
||||||
|
closeOnBlur={true}
|
||||||
|
initialFocusRef={inputRef}
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Box>
|
||||||
|
<Tooltip label="Overlay commodity data" placement="top" hasArrow>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
leftIcon={<Database size={14} />}
|
||||||
|
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
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
bg="#1a1a2e"
|
||||||
|
borderColor={darkGoldTheme.border}
|
||||||
|
boxShadow="0 4px 20px rgba(0, 0, 0, 0.5)"
|
||||||
|
w="350px"
|
||||||
|
_focus={{ outline: 'none' }}
|
||||||
|
>
|
||||||
|
<PopoverBody p={3}>
|
||||||
|
{/* Search input */}
|
||||||
|
<InputGroup size="sm" mb={3}>
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<SearchIcon color={darkGoldTheme.textMuted} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder="Search commodity metrics..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
bg="rgba(255,255,255,0.05)"
|
||||||
|
borderColor={darkGoldTheme.border}
|
||||||
|
color={darkGoldTheme.textPrimary}
|
||||||
|
_placeholder={{ color: darkGoldTheme.textMuted }}
|
||||||
|
_hover={{ borderColor: darkGoldTheme.gold }}
|
||||||
|
_focus={{ borderColor: darkGoldTheme.gold, boxShadow: 'none' }}
|
||||||
|
/>
|
||||||
|
{(searchQuery || isSearching) && (
|
||||||
|
<InputRightElement>
|
||||||
|
{isSearching || isLoadingData ? (
|
||||||
|
<Spinner size="xs" color={darkGoldTheme.gold} />
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<CloseIcon w="8px" h="8px" />}
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
}}
|
||||||
|
aria-label="Clear"
|
||||||
|
_hover={{ bg: 'transparent' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InputRightElement>
|
||||||
|
)}
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Search results */}
|
||||||
|
{searchResults.length > 0 ? (
|
||||||
|
<List spacing={0} maxH="300px" overflowY="auto">
|
||||||
|
{searchResults.map((metric) => {
|
||||||
|
const isAdded = overlayMetrics.some(m => m.metric_id === metric.metric_id);
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={metric.metric_id}
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
cursor={isAdded ? 'default' : 'pointer'}
|
||||||
|
opacity={isAdded ? 0.5 : 1}
|
||||||
|
_hover={isAdded ? {} : { bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||||
|
onClick={() => !isAdded && handleSelectMetric(metric)}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<VStack align="start" spacing={0} flex={1}>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
color={darkGoldTheme.gold}
|
||||||
|
fontWeight="medium"
|
||||||
|
noOfLines={1}
|
||||||
|
>
|
||||||
|
{metric.metric_name}
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||||
|
{metric.source}
|
||||||
|
</Text>
|
||||||
|
{metric.frequency && (
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
bg="rgba(212, 175, 55, 0.1)"
|
||||||
|
color={darkGoldTheme.gold}
|
||||||
|
fontSize="10px"
|
||||||
|
>
|
||||||
|
{metric.frequency}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{metric.unit && (
|
||||||
|
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||||
|
{metric.unit}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
{isAdded ? (
|
||||||
|
<Badge colorScheme="green" fontSize="10px">Added</Badge>
|
||||||
|
) : (
|
||||||
|
<AddIcon boxSize={3} color={darkGoldTheme.textMuted} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
) : searchQuery && !isSearching ? (
|
||||||
|
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center" py={4}>
|
||||||
|
No metrics found
|
||||||
|
</Text>
|
||||||
|
) : !searchQuery ? (
|
||||||
|
<VStack spacing={2} py={4}>
|
||||||
|
<TrendingUp size={24} color={darkGoldTheme.textMuted} />
|
||||||
|
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">
|
||||||
|
Enter keywords to search
|
||||||
|
</Text>
|
||||||
|
<Text color={darkGoldTheme.textMuted} fontSize="xs" textAlign="center">
|
||||||
|
Supports SMM and Mysteel data sources
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
) : null}
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetricOverlaySearch;
|
||||||
@@ -351,6 +351,18 @@ export interface AnalysisModalContentProps {
|
|||||||
theme: Theme;
|
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 返回值
|
* useMarketData Hook 返回值
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
FundingDayData,
|
FundingDayData,
|
||||||
PledgeData,
|
PledgeData,
|
||||||
RiseAnalysis,
|
RiseAnalysis,
|
||||||
|
OverlayMetricData,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { formatNumber } from './formatUtils';
|
import { formatNumber } from './formatUtils';
|
||||||
|
|
||||||
@@ -856,6 +857,7 @@ export const calculateTrendLine = (
|
|||||||
* @param mainIndicator 主图指标 (默认MA)
|
* @param mainIndicator 主图指标 (默认MA)
|
||||||
* @param showAnalysis 是否显示涨幅分析标记 (默认true)
|
* @param showAnalysis 是否显示涨幅分析标记 (默认true)
|
||||||
* @param drawingType 绘图工具类型 (默认NONE)
|
* @param drawingType 绘图工具类型 (默认NONE)
|
||||||
|
* @param overlayMetrics 叠加的商品指标数据 (可选)
|
||||||
*/
|
*/
|
||||||
export const getKLineDarkGoldOption = (
|
export const getKLineDarkGoldOption = (
|
||||||
tradeData: TradeDayData[],
|
tradeData: TradeDayData[],
|
||||||
@@ -863,7 +865,8 @@ export const getKLineDarkGoldOption = (
|
|||||||
subIndicator: IndicatorType = 'MACD',
|
subIndicator: IndicatorType = 'MACD',
|
||||||
mainIndicator: MainIndicatorType = 'MA',
|
mainIndicator: MainIndicatorType = 'MA',
|
||||||
showAnalysis: boolean = true,
|
showAnalysis: boolean = true,
|
||||||
drawingType: DrawingType = 'NONE'
|
drawingType: DrawingType = 'NONE',
|
||||||
|
overlayMetrics: OverlayMetricData[] = []
|
||||||
): EChartsOption => {
|
): EChartsOption => {
|
||||||
if (!tradeData || tradeData.length === 0) return {};
|
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<string, number> = {};
|
||||||
|
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线'];
|
const legendData = ['K线'];
|
||||||
if (mainIndicator === 'MA') {
|
if (mainIndicator === 'MA') {
|
||||||
@@ -1458,6 +1520,33 @@ export const getKLineDarkGoldOption = (
|
|||||||
legendData.push('BIAS6', 'BIAS12', 'BIAS24');
|
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 {
|
return {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
animation: true,
|
animation: true,
|
||||||
@@ -1500,7 +1589,14 @@ export const getKLineDarkGoldOption = (
|
|||||||
// 跳过涨幅分析标记
|
// 跳过涨幅分析标记
|
||||||
} else if (typeof value === 'number' || (value !== null && !isNaN(Number(value)))) {
|
} else if (typeof value === 'number' || (value !== null && !isNaN(Number(value)))) {
|
||||||
// MA、BOLL、MACD、KDJ、RSI 等指标
|
// MA、BOLL、MACD、KDJ、RSI 等指标
|
||||||
result += `${marker} ${seriesName}:${formatPrice(Number(value))}<br/>`;
|
// 检查是否是叠加指标
|
||||||
|
const overlayItem = overlaySeriesData.find(o => o.name === seriesName);
|
||||||
|
if (overlayItem) {
|
||||||
|
// 叠加指标,显示单位
|
||||||
|
result += `${marker} <span style="color: ${overlayItem.color}">${seriesName}:${formatPrice(Number(value))}${overlayItem.unit ? ' ' + overlayItem.unit : ''}</span><br/>`;
|
||||||
|
} else {
|
||||||
|
result += `${marker} ${seriesName}:${formatPrice(Number(value))}<br/>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user