From b25d48e1678c3db6dd1af4e1468648195c8b2edb Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 16 Dec 2025 16:15:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(StockQuoteCard):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=82=A1=E7=A5=A8=E5=AF=B9=E6=AF=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CompareStockInput: 股票搜索输入组件 - 新增 StockCompareModal: 股票对比弹窗 - 更新类型定义支持对比功能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/CompareStockInput.tsx | 220 ++++++++++++++++ .../components/StockCompareModal.tsx | 244 ++++++++++++++++++ .../StockQuoteCard/components/index.ts | 6 + .../components/StockQuoteCard/index.tsx | 42 ++- .../components/StockQuoteCard/types.ts | 10 + 5 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 src/views/Company/components/StockQuoteCard/components/CompareStockInput.tsx create mode 100644 src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx create mode 100644 src/views/Company/components/StockQuoteCard/components/index.ts diff --git a/src/views/Company/components/StockQuoteCard/components/CompareStockInput.tsx b/src/views/Company/components/StockQuoteCard/components/CompareStockInput.tsx new file mode 100644 index 00000000..94bf667e --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/components/CompareStockInput.tsx @@ -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 = ({ + onCompare, + isLoading = false, + currentStockCode, +}) => { + const [inputValue, setInputValue] = useState(''); + const [showDropdown, setShowDropdown] = useState(false); + const [filteredStocks, setFilteredStocks] = useState([]); + const [selectedStock, setSelectedStock] = useState(null); + const containerRef = useRef(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 ( + + + + + + + { + 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, + }} + /> + + + + + + {/* 模糊搜索下拉列表 */} + {showDropdown && ( + + + {filteredStocks.map((stock) => ( + handleSelectStock(stock)} + borderBottom="1px solid" + borderColor="whiteAlpha.100" + _last={{ borderBottom: 'none' }} + > + + + {stock.code} + + + {stock.name} + + + + ))} + + + )} + + ); +}; + +export default CompareStockInput; diff --git a/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx new file mode 100644 index 00000000..b56c0a1a --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx @@ -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 = ({ + 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 ( + + + + 股票对比 + + +
+ {isLoading ? ( + + + 加载对比数据中... + + ) : ( + 暂无对比数据 + )} +
+
+
+
+ ); + } + + return ( + + + + + {currentStockInfo?.stock_name} ({currentStock}) vs {compareStockInfo?.stock_name} ({compareStock}) + + + + + {/* 对比明细表格 */} + + + 对比明细 + + + + + + + + + + + + + + {COMPARE_METRICS.map((metric) => { + const value1 = getValueByPath(currentStockInfo, metric.path); + const value2 = getValueByPath(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 ( + + + + + + + ); + })} + +
指标{currentStockInfo?.stock_name}{compareStockInfo?.stock_name}差异
{metric.label} + {metric.format === 'percent' + ? formatUtils.formatPercent(value1) + : formatUtils.formatLargeNumber(value1)} + + {metric.format === 'percent' + ? formatUtils.formatPercent(value2) + : formatUtils.formatLargeNumber(value2)} + + {diff !== null ? ( + + {diff > 0 && } + {diff < 0 && } + + {metric.format === 'percent' + ? `${Math.abs(diff).toFixed(2)}pp` + : `${Math.abs(diff).toFixed(2)}%`} + + + ) : ( + '-' + )} +
+
+
+
+ + {/* 对比图表 */} + + + + + 盈利能力对比 + + + + + + + + + + + 成长能力对比 + + + + + + + +
+
+
+
+ ); +}; + +export default StockCompareModal; diff --git a/src/views/Company/components/StockQuoteCard/components/index.ts b/src/views/Company/components/StockQuoteCard/components/index.ts new file mode 100644 index 00000000..da8d66e2 --- /dev/null +++ b/src/views/Company/components/StockQuoteCard/components/index.ts @@ -0,0 +1,6 @@ +/** + * StockQuoteCard 子组件导出 + */ + +export { default as CompareStockInput } from './CompareStockInput'; +export { default as StockCompareModal } from './StockCompareModal'; diff --git a/src/views/Company/components/StockQuoteCard/index.tsx b/src/views/Company/components/StockQuoteCard/index.tsx index 49d70b24..0ed417d6 100644 --- a/src/views/Company/components/StockQuoteCard/index.tsx +++ b/src/views/Company/components/StockQuoteCard/index.tsx @@ -21,11 +21,13 @@ import { Divider, Link, Icon, + useDisclosure, } from '@chakra-ui/react'; import { Share2, Calendar, Coins, MapPin, Globe } from 'lucide-react'; import { formatRegisteredCapital, formatDate } from '../CompanyOverview/utils'; import FavoriteButton from '@components/FavoriteButton'; +import { CompareStockInput, StockCompareModal } from './components'; import type { StockQuoteCardProps } from './types'; /** @@ -62,12 +64,33 @@ const StockQuoteCard: React.FC = ({ onWatchlistToggle, onShare, basicInfo, + // 对比相关 + currentStockInfo, + compareStockInfo, + isCompareLoading = false, + onCompare, + onCloseCompare, }) => { + // 对比弹窗控制 + const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure(); + // 处理分享点击 const handleShare = () => { onShare?.(); }; + // 处理对比按钮点击 + const handleCompare = (stockCode: string) => { + onCompare?.(stockCode); + openCompareModal(); + }; + + // 处理关闭对比弹窗 + const handleCloseCompare = () => { + closeCompareModal(); + onCloseCompare?.(); + }; + // 黑金主题颜色配置 const cardBg = '#1A202C'; const borderColor = '#C9A961'; @@ -139,8 +162,14 @@ const StockQuoteCard: React.FC = ({ )} - {/* 右侧:关注 + 分享 + 时间 */} + {/* 右侧:对比 + 关注 + 分享 + 时间 */} + {/* 股票对比输入 */} + = ({ + {/* 股票对比弹窗 */} + + {/* 1:2 布局 */} {/* 左栏:价格信息 (flex=1) */} diff --git a/src/views/Company/components/StockQuoteCard/types.ts b/src/views/Company/components/StockQuoteCard/types.ts index a1d2a788..133e138f 100644 --- a/src/views/Company/components/StockQuoteCard/types.ts +++ b/src/views/Company/components/StockQuoteCard/types.ts @@ -3,6 +3,7 @@ */ import type { BasicInfo } from '../CompanyOverview/types'; +import type { StockInfo } from '../FinancialPanorama/types'; /** * 股票行情卡片数据 @@ -57,4 +58,13 @@ export interface StockQuoteCardProps { onShare?: () => void; // 分享回调 // 公司基本信息 basicInfo?: BasicInfo; + // 股票对比相关 + currentStockInfo?: StockInfo; // 当前股票财务信息(用于对比) + compareStockInfo?: StockInfo; // 对比股票财务信息 + isCompareLoading?: boolean; // 对比数据加载中 + onCompare?: (stockCode: string) => void; // 触发对比回调 + onCloseCompare?: () => void; // 关闭对比弹窗回调 } + +// 重新导出 StockInfo 类型以便外部使用 +export type { StockInfo };