feat(StockQuoteCard): 新增股票对比功能
- 新增 CompareStockInput: 股票搜索输入组件 - 新增 StockCompareModal: 股票对比弹窗 - 更新类型定义支持对比功能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* CompareStockInput - 对比股票输入组件
|
||||||
|
* 紧凑型输入框,支持模糊搜索下拉
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
Spinner,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { SearchIcon } from '@chakra-ui/icons';
|
||||||
|
import { BarChart2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface CompareStockInputProps {
|
||||||
|
onCompare: (stockCode: string) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
currentStockCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Stock {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RootState {
|
||||||
|
stock: {
|
||||||
|
allStocks: Stock[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CompareStockInput: React.FC<CompareStockInputProps> = ({
|
||||||
|
onCompare,
|
||||||
|
isLoading = false,
|
||||||
|
currentStockCode,
|
||||||
|
}) => {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const [filteredStocks, setFilteredStocks] = useState<Stock[]>([]);
|
||||||
|
const [selectedStock, setSelectedStock] = useState<Stock | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 从 Redux 获取全部股票列表
|
||||||
|
const allStocks = useSelector((state: RootState) => state.stock.allStocks);
|
||||||
|
|
||||||
|
// 黑金主题颜色
|
||||||
|
const borderColor = '#C9A961';
|
||||||
|
const goldColor = '#F4D03F';
|
||||||
|
const bgColor = '#1A202C';
|
||||||
|
|
||||||
|
// 模糊搜索过滤
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputValue && inputValue.trim()) {
|
||||||
|
const searchTerm = inputValue.trim().toLowerCase();
|
||||||
|
const filtered = allStocks
|
||||||
|
.filter(
|
||||||
|
(stock) =>
|
||||||
|
stock.code !== currentStockCode && // 排除当前股票
|
||||||
|
(stock.code.toLowerCase().includes(searchTerm) ||
|
||||||
|
stock.name.includes(inputValue.trim()))
|
||||||
|
)
|
||||||
|
.slice(0, 8); // 限制显示8条
|
||||||
|
setFilteredStocks(filtered);
|
||||||
|
setShowDropdown(filtered.length > 0);
|
||||||
|
} else {
|
||||||
|
setFilteredStocks([]);
|
||||||
|
setShowDropdown(false);
|
||||||
|
}
|
||||||
|
}, [inputValue, allStocks, currentStockCode]);
|
||||||
|
|
||||||
|
// 点击外部关闭下拉
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 选择股票
|
||||||
|
const handleSelectStock = (stock: Stock) => {
|
||||||
|
setSelectedStock(stock);
|
||||||
|
setInputValue(stock.name);
|
||||||
|
setShowDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理对比按钮点击
|
||||||
|
const handleCompare = () => {
|
||||||
|
if (selectedStock) {
|
||||||
|
onCompare(selectedStock.code);
|
||||||
|
} else if (inputValue.trim().length === 6 && /^\d{6}$/.test(inputValue.trim())) {
|
||||||
|
// 如果直接输入了6位数字代码
|
||||||
|
onCompare(inputValue.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setShowDropdown(false);
|
||||||
|
handleCompare();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isButtonDisabled = !selectedStock && !(inputValue.trim().length === 6 && /^\d{6}$/.test(inputValue.trim()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={containerRef} position="relative">
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<InputGroup size="sm" w="160px">
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<SearchIcon color={borderColor} boxSize={3} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
placeholder="对比股票"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
setSelectedStock(null);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => inputValue && filteredStocks.length > 0 && setShowDropdown(true)}
|
||||||
|
borderRadius="md"
|
||||||
|
color="white"
|
||||||
|
fontSize="sm"
|
||||||
|
borderColor={borderColor}
|
||||||
|
bg="transparent"
|
||||||
|
_placeholder={{ color: borderColor, fontSize: 'sm' }}
|
||||||
|
_focus={{
|
||||||
|
borderColor: goldColor,
|
||||||
|
boxShadow: `0 0 0 1px ${goldColor}`,
|
||||||
|
}}
|
||||||
|
_hover={{
|
||||||
|
borderColor: goldColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
leftIcon={isLoading ? <Spinner size="xs" /> : <BarChart2 size={14} />}
|
||||||
|
onClick={handleCompare}
|
||||||
|
isDisabled={isButtonDisabled || isLoading}
|
||||||
|
bg="transparent"
|
||||||
|
color={goldColor}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={borderColor}
|
||||||
|
_hover={{
|
||||||
|
bg: 'whiteAlpha.100',
|
||||||
|
borderColor: goldColor,
|
||||||
|
}}
|
||||||
|
_disabled={{
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
}}
|
||||||
|
fontSize="sm"
|
||||||
|
px={3}
|
||||||
|
>
|
||||||
|
对比
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 模糊搜索下拉列表 */}
|
||||||
|
{showDropdown && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="100%"
|
||||||
|
left={0}
|
||||||
|
mt={1}
|
||||||
|
w="220px"
|
||||||
|
bg={bgColor}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="md"
|
||||||
|
maxH="240px"
|
||||||
|
overflowY="auto"
|
||||||
|
zIndex={1000}
|
||||||
|
boxShadow="0 4px 12px rgba(0, 0, 0, 0.3)"
|
||||||
|
>
|
||||||
|
<VStack align="stretch" spacing={0}>
|
||||||
|
{filteredStocks.map((stock) => (
|
||||||
|
<Box
|
||||||
|
key={stock.code}
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100' }}
|
||||||
|
onClick={() => handleSelectStock(stock)}
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="whiteAlpha.100"
|
||||||
|
_last={{ borderBottom: 'none' }}
|
||||||
|
>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={goldColor} fontWeight="bold" fontSize="xs">
|
||||||
|
{stock.code}
|
||||||
|
</Text>
|
||||||
|
<Text color={borderColor} fontSize="xs" noOfLines={1} maxW="120px">
|
||||||
|
{stock.name}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompareStockInput;
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* StockCompareModal - 股票对比弹窗组件
|
||||||
|
* 展示对比明细、盈利能力对比、成长力对比
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
TableContainer,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ArrowUpIcon, ArrowDownIcon } from '@chakra-ui/icons';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
|
||||||
|
import { COMPARE_METRICS } from '../../FinancialPanorama/constants';
|
||||||
|
import { getValueByPath, getCompareBarChartOption } from '../../FinancialPanorama/utils';
|
||||||
|
import { formatUtils } from '@services/financialService';
|
||||||
|
import type { StockInfo } from '../../FinancialPanorama/types';
|
||||||
|
|
||||||
|
interface StockCompareModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
currentStock: string;
|
||||||
|
currentStockInfo: StockInfo | null;
|
||||||
|
compareStock: string;
|
||||||
|
compareStockInfo: StockInfo | null;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockCompareModal: React.FC<StockCompareModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentStock,
|
||||||
|
currentStockInfo,
|
||||||
|
compareStock,
|
||||||
|
compareStockInfo,
|
||||||
|
isLoading = false,
|
||||||
|
}) => {
|
||||||
|
// 黑金主题颜色
|
||||||
|
const bgColor = '#1A202C';
|
||||||
|
const borderColor = '#C9A961';
|
||||||
|
const goldColor = '#F4D03F';
|
||||||
|
const positiveColor = '#EF4444'; // 红涨
|
||||||
|
const negativeColor = '#10B981'; // 绿跌
|
||||||
|
|
||||||
|
// 加载中或无数据时的显示
|
||||||
|
if (isLoading || !currentStockInfo || !compareStockInfo) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||||
|
<ModalHeader color={goldColor}>股票对比</ModalHeader>
|
||||||
|
<ModalCloseButton color={borderColor} />
|
||||||
|
<ModalBody pb={6}>
|
||||||
|
<Center py={20}>
|
||||||
|
{isLoading ? (
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Spinner size="xl" color={goldColor} />
|
||||||
|
<Text color={borderColor}>加载对比数据中...</Text>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<Text color={borderColor}>暂无对比数据</Text>
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||||
|
<ModalHeader color={goldColor}>
|
||||||
|
{currentStockInfo?.stock_name} ({currentStock}) vs {compareStockInfo?.stock_name} ({compareStock})
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color={borderColor} />
|
||||||
|
<ModalBody pb={6}>
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* 对比明细表格 */}
|
||||||
|
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||||
|
<CardHeader pb={2}>
|
||||||
|
<Heading size="sm" color={goldColor}>对比明细</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody pt={0}>
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="sm" variant="unstyled">
|
||||||
|
<Thead>
|
||||||
|
<Tr borderBottom="1px solid" borderColor={borderColor}>
|
||||||
|
<Th color={borderColor} fontSize="xs">指标</Th>
|
||||||
|
<Th isNumeric color={borderColor} fontSize="xs">{currentStockInfo?.stock_name}</Th>
|
||||||
|
<Th isNumeric color={borderColor} fontSize="xs">{compareStockInfo?.stock_name}</Th>
|
||||||
|
<Th isNumeric color={borderColor} fontSize="xs">差异</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{COMPARE_METRICS.map((metric) => {
|
||||||
|
const value1 = getValueByPath<number>(currentStockInfo, metric.path);
|
||||||
|
const value2 = getValueByPath<number>(compareStockInfo, metric.path);
|
||||||
|
|
||||||
|
let diff: number | null = null;
|
||||||
|
let diffColor = borderColor;
|
||||||
|
|
||||||
|
if (value1 !== undefined && value2 !== undefined && value1 !== null && value2 !== null) {
|
||||||
|
if (metric.format === 'percent') {
|
||||||
|
diff = value1 - value2;
|
||||||
|
diffColor = diff > 0 ? positiveColor : negativeColor;
|
||||||
|
} else if (value2 !== 0) {
|
||||||
|
diff = ((value1 - value2) / Math.abs(value2)) * 100;
|
||||||
|
diffColor = diff > 0 ? positiveColor : negativeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={metric.key} borderBottom="1px solid" borderColor="whiteAlpha.100">
|
||||||
|
<Td color={borderColor} fontSize="sm">{metric.label}</Td>
|
||||||
|
<Td isNumeric color={goldColor} fontSize="sm">
|
||||||
|
{metric.format === 'percent'
|
||||||
|
? formatUtils.formatPercent(value1)
|
||||||
|
: formatUtils.formatLargeNumber(value1)}
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric color={goldColor} fontSize="sm">
|
||||||
|
{metric.format === 'percent'
|
||||||
|
? formatUtils.formatPercent(value2)
|
||||||
|
: formatUtils.formatLargeNumber(value2)}
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric color={diffColor} fontSize="sm">
|
||||||
|
{diff !== null ? (
|
||||||
|
<HStack spacing={1} justify="flex-end">
|
||||||
|
{diff > 0 && <ArrowUpIcon boxSize={3} />}
|
||||||
|
{diff < 0 && <ArrowDownIcon boxSize={3} />}
|
||||||
|
<Text>
|
||||||
|
{metric.format === 'percent'
|
||||||
|
? `${Math.abs(diff).toFixed(2)}pp`
|
||||||
|
: `${Math.abs(diff).toFixed(2)}%`}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 对比图表 */}
|
||||||
|
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
|
||||||
|
<GridItem>
|
||||||
|
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||||
|
<CardHeader pb={2}>
|
||||||
|
<Heading size="sm" color={goldColor}>盈利能力对比</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody pt={0}>
|
||||||
|
<ReactECharts
|
||||||
|
option={getCompareBarChartOption(
|
||||||
|
'盈利能力对比',
|
||||||
|
currentStockInfo?.stock_name || '',
|
||||||
|
compareStockInfo?.stock_name || '',
|
||||||
|
['ROE', 'ROA', '毛利率', '净利率'],
|
||||||
|
[
|
||||||
|
currentStockInfo?.key_metrics?.roe,
|
||||||
|
currentStockInfo?.key_metrics?.roa,
|
||||||
|
currentStockInfo?.key_metrics?.gross_margin,
|
||||||
|
currentStockInfo?.key_metrics?.net_margin,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
compareStockInfo?.key_metrics?.roe,
|
||||||
|
compareStockInfo?.key_metrics?.roa,
|
||||||
|
compareStockInfo?.key_metrics?.gross_margin,
|
||||||
|
compareStockInfo?.key_metrics?.net_margin,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
style={{ height: '280px' }}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem>
|
||||||
|
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||||
|
<CardHeader pb={2}>
|
||||||
|
<Heading size="sm" color={goldColor}>成长能力对比</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody pt={0}>
|
||||||
|
<ReactECharts
|
||||||
|
option={getCompareBarChartOption(
|
||||||
|
'成长能力对比',
|
||||||
|
currentStockInfo?.stock_name || '',
|
||||||
|
compareStockInfo?.stock_name || '',
|
||||||
|
['营收增长', '利润增长', '资产增长', '股东权益增长'],
|
||||||
|
[
|
||||||
|
currentStockInfo?.growth_rates?.revenue_growth,
|
||||||
|
currentStockInfo?.growth_rates?.profit_growth,
|
||||||
|
currentStockInfo?.growth_rates?.asset_growth,
|
||||||
|
currentStockInfo?.growth_rates?.equity_growth,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
compareStockInfo?.growth_rates?.revenue_growth,
|
||||||
|
compareStockInfo?.growth_rates?.profit_growth,
|
||||||
|
compareStockInfo?.growth_rates?.asset_growth,
|
||||||
|
compareStockInfo?.growth_rates?.equity_growth,
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
style={{ height: '280px' }}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockCompareModal;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* StockQuoteCard 子组件导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as CompareStockInput } from './CompareStockInput';
|
||||||
|
export { default as StockCompareModal } from './StockCompareModal';
|
||||||
@@ -21,11 +21,13 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Link,
|
Link,
|
||||||
Icon,
|
Icon,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Share2, Calendar, Coins, MapPin, Globe } from 'lucide-react';
|
import { Share2, Calendar, Coins, MapPin, Globe } from 'lucide-react';
|
||||||
import { formatRegisteredCapital, formatDate } from '../CompanyOverview/utils';
|
import { formatRegisteredCapital, formatDate } from '../CompanyOverview/utils';
|
||||||
|
|
||||||
import FavoriteButton from '@components/FavoriteButton';
|
import FavoriteButton from '@components/FavoriteButton';
|
||||||
|
import { CompareStockInput, StockCompareModal } from './components';
|
||||||
import type { StockQuoteCardProps } from './types';
|
import type { StockQuoteCardProps } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,12 +64,33 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
onWatchlistToggle,
|
onWatchlistToggle,
|
||||||
onShare,
|
onShare,
|
||||||
basicInfo,
|
basicInfo,
|
||||||
|
// 对比相关
|
||||||
|
currentStockInfo,
|
||||||
|
compareStockInfo,
|
||||||
|
isCompareLoading = false,
|
||||||
|
onCompare,
|
||||||
|
onCloseCompare,
|
||||||
}) => {
|
}) => {
|
||||||
|
// 对比弹窗控制
|
||||||
|
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
|
||||||
|
|
||||||
// 处理分享点击
|
// 处理分享点击
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
onShare?.();
|
onShare?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理对比按钮点击
|
||||||
|
const handleCompare = (stockCode: string) => {
|
||||||
|
onCompare?.(stockCode);
|
||||||
|
openCompareModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理关闭对比弹窗
|
||||||
|
const handleCloseCompare = () => {
|
||||||
|
closeCompareModal();
|
||||||
|
onCloseCompare?.();
|
||||||
|
};
|
||||||
|
|
||||||
// 黑金主题颜色配置
|
// 黑金主题颜色配置
|
||||||
const cardBg = '#1A202C';
|
const cardBg = '#1A202C';
|
||||||
const borderColor = '#C9A961';
|
const borderColor = '#C9A961';
|
||||||
@@ -139,8 +162,14 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 右侧:关注 + 分享 + 时间 */}
|
{/* 右侧:对比 + 关注 + 分享 + 时间 */}
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
|
{/* 股票对比输入 */}
|
||||||
|
<CompareStockInput
|
||||||
|
onCompare={handleCompare}
|
||||||
|
isLoading={isCompareLoading}
|
||||||
|
currentStockCode={data.code}
|
||||||
|
/>
|
||||||
<FavoriteButton
|
<FavoriteButton
|
||||||
isFavorite={isInWatchlist}
|
isFavorite={isInWatchlist}
|
||||||
isLoading={isWatchlistLoading}
|
isLoading={isWatchlistLoading}
|
||||||
@@ -165,6 +194,17 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* 股票对比弹窗 */}
|
||||||
|
<StockCompareModal
|
||||||
|
isOpen={isCompareModalOpen}
|
||||||
|
onClose={handleCloseCompare}
|
||||||
|
currentStock={data.code}
|
||||||
|
currentStockInfo={currentStockInfo || null}
|
||||||
|
compareStock={compareStockInfo?.stock_code || ''}
|
||||||
|
compareStockInfo={compareStockInfo || null}
|
||||||
|
isLoading={isCompareLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 1:2 布局 */}
|
{/* 1:2 布局 */}
|
||||||
<Flex gap={8}>
|
<Flex gap={8}>
|
||||||
{/* 左栏:价格信息 (flex=1) */}
|
{/* 左栏:价格信息 (flex=1) */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BasicInfo } from '../CompanyOverview/types';
|
import type { BasicInfo } from '../CompanyOverview/types';
|
||||||
|
import type { StockInfo } from '../FinancialPanorama/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 股票行情卡片数据
|
* 股票行情卡片数据
|
||||||
@@ -57,4 +58,13 @@ export interface StockQuoteCardProps {
|
|||||||
onShare?: () => void; // 分享回调
|
onShare?: () => void; // 分享回调
|
||||||
// 公司基本信息
|
// 公司基本信息
|
||||||
basicInfo?: BasicInfo;
|
basicInfo?: BasicInfo;
|
||||||
|
// 股票对比相关
|
||||||
|
currentStockInfo?: StockInfo; // 当前股票财务信息(用于对比)
|
||||||
|
compareStockInfo?: StockInfo; // 对比股票财务信息
|
||||||
|
isCompareLoading?: boolean; // 对比数据加载中
|
||||||
|
onCompare?: (stockCode: string) => void; // 触发对比回调
|
||||||
|
onCloseCompare?: () => void; // 关闭对比弹窗回调
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新导出 StockInfo 类型以便外部使用
|
||||||
|
export type { StockInfo };
|
||||||
|
|||||||
Reference in New Issue
Block a user