refactor(StockQuoteCard): 拆分为原子组件
- 新增 theme.ts 黑金主题常量 - 新增 formatters.ts 格式化工具函数 - 拆分 PriceDisplay/SecondaryQuote/KeyMetrics/MainForceInfo/CompanyInfo/StockHeader - 主组件从 414 行简化为 150 行 - 提高可维护性和复用性
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* CompanyInfo - 公司信息原子组件
|
||||||
|
* 显示公司基本信息(成立日期、注册资本、所在地、官网、简介)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Box, Flex, HStack, Text, Link, Icon, Divider } from '@chakra-ui/react';
|
||||||
|
import { Calendar, Coins, MapPin, Globe } from 'lucide-react';
|
||||||
|
import { formatRegisteredCapital, formatDate } from '../../CompanyOverview/utils';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface CompanyBasicInfo {
|
||||||
|
establish_date?: string;
|
||||||
|
reg_capital?: number;
|
||||||
|
province?: string;
|
||||||
|
city?: string;
|
||||||
|
website?: string;
|
||||||
|
company_intro?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyInfoProps {
|
||||||
|
basicInfo: CompanyBasicInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompanyInfo: React.FC<CompanyInfoProps> = memo(({ basicInfo }) => {
|
||||||
|
const { labelColor, valueColor, borderColor } = STOCK_CARD_THEME;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider borderColor={borderColor} my={4} />
|
||||||
|
<Flex gap={8}>
|
||||||
|
{/* 左侧:公司关键属性 (flex=1) */}
|
||||||
|
<Box flex="1" minWidth="0">
|
||||||
|
<HStack spacing={4} flexWrap="wrap" fontSize="14px">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={Calendar} color={labelColor} boxSize={4} />
|
||||||
|
<Text color={labelColor}>成立:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold">
|
||||||
|
{formatDate(basicInfo.establish_date)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={Coins} color={labelColor} boxSize={4} />
|
||||||
|
<Text color={labelColor}>注册资本:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold">
|
||||||
|
{formatRegisteredCapital(basicInfo.reg_capital)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={MapPin} color={labelColor} boxSize={4} />
|
||||||
|
<Text color={labelColor}>所在地:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold">
|
||||||
|
{basicInfo.province} {basicInfo.city}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={Globe} color={labelColor} boxSize={4} />
|
||||||
|
{basicInfo.website ? (
|
||||||
|
<Link
|
||||||
|
href={basicInfo.website}
|
||||||
|
isExternal
|
||||||
|
color={valueColor}
|
||||||
|
fontWeight="bold"
|
||||||
|
_hover={{ color: labelColor }}
|
||||||
|
>
|
||||||
|
访问官网
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Text color={valueColor}>暂无官网</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 右侧:公司简介 (flex=2) */}
|
||||||
|
<Box flex="2" minWidth="0" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
||||||
|
<Text fontSize="14px" color={labelColor} noOfLines={2}>
|
||||||
|
<Text as="span" fontWeight="bold" color={valueColor}>公司简介:</Text>
|
||||||
|
{basicInfo.company_intro || '暂无'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CompanyInfo.displayName = 'CompanyInfo';
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* KeyMetrics - 关键指标原子组件
|
||||||
|
* 显示 PE、EPS、PB、流通市值、52周波动
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Box, VStack, HStack, Text } from '@chakra-ui/react';
|
||||||
|
import { formatPrice } from './formatters';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface KeyMetricsProps {
|
||||||
|
pe: number;
|
||||||
|
eps?: number;
|
||||||
|
pb: number;
|
||||||
|
marketCap: string;
|
||||||
|
week52Low: number;
|
||||||
|
week52High: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
|
||||||
|
pe,
|
||||||
|
eps,
|
||||||
|
pb,
|
||||||
|
marketCap,
|
||||||
|
week52Low,
|
||||||
|
week52High,
|
||||||
|
}) => {
|
||||||
|
const { labelColor, valueColor, sectionTitleColor } = STOCK_CARD_THEME;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flex="1">
|
||||||
|
<Text
|
||||||
|
fontSize="14px"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={sectionTitleColor}
|
||||||
|
mb={3}
|
||||||
|
>
|
||||||
|
关键指标
|
||||||
|
</Text>
|
||||||
|
<VStack align="stretch" spacing={2} fontSize="14px">
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>市盈率(PE):</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{pe.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>每股收益(EPS):</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{eps?.toFixed(3) || '-'}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>市净率(PB):</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{pb.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>流通市值:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{marketCap}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>52周波动:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{formatPrice(week52Low)}-{formatPrice(week52High)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
KeyMetrics.displayName = 'KeyMetrics';
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* MainForceInfo - 主力动态原子组件
|
||||||
|
* 显示主力净流入、机构持仓、买卖比例
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Box, VStack, HStack, Text, Progress } from '@chakra-ui/react';
|
||||||
|
import { formatNetInflow } from './formatters';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface MainForceInfoProps {
|
||||||
|
mainNetInflow: number;
|
||||||
|
institutionHolding: number;
|
||||||
|
buyRatio: number;
|
||||||
|
sellRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainForceInfo: React.FC<MainForceInfoProps> = memo(({
|
||||||
|
mainNetInflow,
|
||||||
|
institutionHolding,
|
||||||
|
buyRatio,
|
||||||
|
sellRatio,
|
||||||
|
}) => {
|
||||||
|
const { labelColor, valueColor, sectionTitleColor, borderColor, upColor, downColor } = STOCK_CARD_THEME;
|
||||||
|
const inflowColor = mainNetInflow >= 0 ? upColor : downColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flex="1" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
||||||
|
<Text
|
||||||
|
fontSize="14px"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={sectionTitleColor}
|
||||||
|
mb={3}
|
||||||
|
>
|
||||||
|
主力动态
|
||||||
|
</Text>
|
||||||
|
<VStack align="stretch" spacing={2} fontSize="14px">
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>主力净流入:</Text>
|
||||||
|
<Text color={inflowColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{formatNetInflow(mainNetInflow)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text color={labelColor}>机构持仓:</Text>
|
||||||
|
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
||||||
|
{institutionHolding.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{/* 买卖比例条 */}
|
||||||
|
<Box mt={1}>
|
||||||
|
<Progress
|
||||||
|
value={buyRatio}
|
||||||
|
size="sm"
|
||||||
|
sx={{
|
||||||
|
'& > div': { bg: upColor },
|
||||||
|
}}
|
||||||
|
bg={downColor}
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
|
<HStack justify="space-between" mt={1} fontSize="14px">
|
||||||
|
<Text color={upColor}>买入{buyRatio}%</Text>
|
||||||
|
<Text color={downColor}>卖出{sellRatio}%</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MainForceInfo.displayName = 'MainForceInfo';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* PriceDisplay - 价格显示原子组件
|
||||||
|
* 显示当前价格和涨跌幅 Badge
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { HStack, Text, Badge } from '@chakra-ui/react';
|
||||||
|
import { formatPrice, formatChangePercent } from './formatters';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface PriceDisplayProps {
|
||||||
|
currentPrice: number;
|
||||||
|
changePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PriceDisplay: React.FC<PriceDisplayProps> = memo(({
|
||||||
|
currentPrice,
|
||||||
|
changePercent,
|
||||||
|
}) => {
|
||||||
|
const { upColor, downColor } = STOCK_CARD_THEME;
|
||||||
|
const priceColor = changePercent >= 0 ? upColor : downColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack align="baseline" spacing={3} mb={3}>
|
||||||
|
<Text fontSize="48px" fontWeight="bold" color={priceColor}>
|
||||||
|
{formatPrice(currentPrice)}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
bg={changePercent >= 0 ? upColor : downColor}
|
||||||
|
color="#FFFFFF"
|
||||||
|
fontSize="20px"
|
||||||
|
fontWeight="bold"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
{formatChangePercent(changePercent)}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PriceDisplay.displayName = 'PriceDisplay';
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* SecondaryQuote - 次要行情原子组件
|
||||||
|
* 显示今开、昨收、最高、最低
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { HStack, Text } from '@chakra-ui/react';
|
||||||
|
import { formatPrice } from './formatters';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface SecondaryQuoteProps {
|
||||||
|
todayOpen: number;
|
||||||
|
yesterdayClose: number;
|
||||||
|
todayHigh: number;
|
||||||
|
todayLow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecondaryQuote: React.FC<SecondaryQuoteProps> = memo(({
|
||||||
|
todayOpen,
|
||||||
|
yesterdayClose,
|
||||||
|
todayHigh,
|
||||||
|
todayLow,
|
||||||
|
}) => {
|
||||||
|
const { labelColor, valueColor, borderColor, upColor, downColor } = STOCK_CARD_THEME;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack spacing={4} fontSize="14px" flexWrap="wrap">
|
||||||
|
<Text color={labelColor}>
|
||||||
|
今开:
|
||||||
|
<Text as="span" color={valueColor} fontWeight="bold">
|
||||||
|
{formatPrice(todayOpen)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text color={borderColor}>|</Text>
|
||||||
|
<Text color={labelColor}>
|
||||||
|
昨收:
|
||||||
|
<Text as="span" color={valueColor} fontWeight="bold">
|
||||||
|
{formatPrice(yesterdayClose)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text color={borderColor}>|</Text>
|
||||||
|
<Text color={labelColor}>
|
||||||
|
最高:
|
||||||
|
<Text as="span" color={upColor} fontWeight="bold">
|
||||||
|
{formatPrice(todayHigh)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text color={borderColor}>|</Text>
|
||||||
|
<Text color={labelColor}>
|
||||||
|
最低:
|
||||||
|
<Text as="span" color={downColor} fontWeight="bold">
|
||||||
|
{formatPrice(todayLow)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SecondaryQuote.displayName = 'SecondaryQuote';
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* StockHeader - 股票头部原子组件
|
||||||
|
* 显示股票名称、代码、行业标签、指数标签、操作按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Flex, HStack, Text, Badge, IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { Share2 } from 'lucide-react';
|
||||||
|
import FavoriteButton from '@components/FavoriteButton';
|
||||||
|
import CompareStockInput from './CompareStockInput';
|
||||||
|
import { STOCK_CARD_THEME } from './theme';
|
||||||
|
|
||||||
|
export interface StockHeaderProps {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
industryL1?: string;
|
||||||
|
industry?: string;
|
||||||
|
indexTags?: string[];
|
||||||
|
updateTime?: string;
|
||||||
|
// 关注相关
|
||||||
|
isInWatchlist?: boolean;
|
||||||
|
isWatchlistLoading?: boolean;
|
||||||
|
onWatchlistToggle?: () => void;
|
||||||
|
// 分享
|
||||||
|
onShare?: () => void;
|
||||||
|
// 对比相关
|
||||||
|
isCompareLoading?: boolean;
|
||||||
|
onCompare?: (stockCode: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StockHeader: React.FC<StockHeaderProps> = memo(({
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
industryL1,
|
||||||
|
industry,
|
||||||
|
indexTags,
|
||||||
|
updateTime,
|
||||||
|
isInWatchlist = false,
|
||||||
|
isWatchlistLoading = false,
|
||||||
|
onWatchlistToggle,
|
||||||
|
onShare,
|
||||||
|
isCompareLoading = false,
|
||||||
|
onCompare,
|
||||||
|
}) => {
|
||||||
|
const { labelColor, valueColor, borderColor } = STOCK_CARD_THEME;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="space-between" align="center" mb={4}>
|
||||||
|
{/* 左侧:股票名称 + 行业标签 + 指数标签 */}
|
||||||
|
<HStack spacing={3} align="center">
|
||||||
|
{/* 股票名称 - 突出显示 */}
|
||||||
|
<Text fontSize="26px" fontWeight="800" color={valueColor}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="18px" fontWeight="normal" color={labelColor}>
|
||||||
|
({code})
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 行业标签 */}
|
||||||
|
{(industryL1 || industry) && (
|
||||||
|
<Badge
|
||||||
|
bg="transparent"
|
||||||
|
color={labelColor}
|
||||||
|
fontSize="14px"
|
||||||
|
fontWeight="medium"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={borderColor}
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
{industryL1 && industry
|
||||||
|
? `${industryL1} · ${industry}`
|
||||||
|
: industry || industryL1}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 指数标签 */}
|
||||||
|
{indexTags && indexTags.length > 0 && (
|
||||||
|
<Text fontSize="14px" color={labelColor}>
|
||||||
|
{indexTags.join('、')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 右侧:对比 + 关注 + 分享 + 时间 */}
|
||||||
|
<HStack spacing={3}>
|
||||||
|
{/* 股票对比输入 */}
|
||||||
|
<CompareStockInput
|
||||||
|
onCompare={onCompare || (() => {})}
|
||||||
|
isLoading={isCompareLoading}
|
||||||
|
currentStockCode={code}
|
||||||
|
/>
|
||||||
|
<FavoriteButton
|
||||||
|
isFavorite={isInWatchlist}
|
||||||
|
isLoading={isWatchlistLoading}
|
||||||
|
onClick={onWatchlistToggle || (() => {})}
|
||||||
|
colorScheme="gold"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Tooltip label="分享" placement="top">
|
||||||
|
<IconButton
|
||||||
|
aria-label="分享"
|
||||||
|
icon={<Share2 size={18} />}
|
||||||
|
variant="ghost"
|
||||||
|
color={labelColor}
|
||||||
|
size="sm"
|
||||||
|
onClick={onShare}
|
||||||
|
_hover={{ bg: 'whiteAlpha.100' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Text fontSize="14px" color={labelColor}>
|
||||||
|
{updateTime?.split(' ')[1] || '--:--'}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
StockHeader.displayName = 'StockHeader';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* StockQuoteCard 格式化工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格显示
|
||||||
|
*/
|
||||||
|
export const formatPrice = (price: number): string => {
|
||||||
|
return price.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化涨跌幅显示
|
||||||
|
*/
|
||||||
|
export const formatChangePercent = (percent: number): string => {
|
||||||
|
const sign = percent >= 0 ? '+' : '';
|
||||||
|
return `${sign}${percent.toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化主力净流入显示
|
||||||
|
*/
|
||||||
|
export const formatNetInflow = (value: number): string => {
|
||||||
|
const sign = value >= 0 ? '+' : '';
|
||||||
|
return `${sign}${value.toFixed(2)}亿`;
|
||||||
|
};
|
||||||
@@ -1,6 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* StockQuoteCard 子组件导出
|
* StockQuoteCard 组件统一导出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 原子组件
|
||||||
|
export { PriceDisplay } from './PriceDisplay';
|
||||||
|
export { SecondaryQuote } from './SecondaryQuote';
|
||||||
|
export { KeyMetrics } from './KeyMetrics';
|
||||||
|
export { MainForceInfo } from './MainForceInfo';
|
||||||
|
export { CompanyInfo } from './CompanyInfo';
|
||||||
|
export { StockHeader } from './StockHeader';
|
||||||
|
|
||||||
|
// 复合组件
|
||||||
export { default as CompareStockInput } from './CompareStockInput';
|
export { default as CompareStockInput } from './CompareStockInput';
|
||||||
export { default as StockCompareModal } from './StockCompareModal';
|
export { default as StockCompareModal } from './StockCompareModal';
|
||||||
|
|
||||||
|
// 工具和主题
|
||||||
|
export { STOCK_CARD_THEME } from './theme';
|
||||||
|
export * from './formatters';
|
||||||
|
|
||||||
|
// 类型导出
|
||||||
|
export type { PriceDisplayProps } from './PriceDisplay';
|
||||||
|
export type { SecondaryQuoteProps } from './SecondaryQuote';
|
||||||
|
export type { KeyMetricsProps } from './KeyMetrics';
|
||||||
|
export type { MainForceInfoProps } from './MainForceInfo';
|
||||||
|
export type { CompanyInfoProps, CompanyBasicInfo } from './CompanyInfo';
|
||||||
|
export type { StockHeaderProps } from './StockHeader';
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* StockQuoteCard 黑金主题配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const STOCK_CARD_THEME = {
|
||||||
|
// 背景和边框
|
||||||
|
cardBg: '#1A202C',
|
||||||
|
borderColor: '#C9A961',
|
||||||
|
|
||||||
|
// 文字颜色
|
||||||
|
labelColor: '#C9A961',
|
||||||
|
valueColor: '#F4D03F',
|
||||||
|
sectionTitleColor: '#F4D03F',
|
||||||
|
|
||||||
|
// 涨跌颜色(红涨绿跌)
|
||||||
|
upColor: '#F44336',
|
||||||
|
downColor: '#4CAF50',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type StockCardTheme = typeof STOCK_CARD_THEME;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
* StockQuoteCard - 股票行情卡片组件
|
* StockQuoteCard - 股票行情卡片组件
|
||||||
*
|
*
|
||||||
* 展示股票的实时行情、关键指标和主力动态
|
* 展示股票的实时行情、关键指标和主力动态
|
||||||
|
* 采用原子组件拆分,提高可维护性和复用性
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -10,52 +11,23 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
Flex,
|
Flex,
|
||||||
HStack,
|
|
||||||
VStack,
|
VStack,
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Progress,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Divider,
|
|
||||||
Link,
|
|
||||||
Icon,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} 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 {
|
||||||
import { CompareStockInput, StockCompareModal } from './components';
|
StockHeader,
|
||||||
|
PriceDisplay,
|
||||||
|
SecondaryQuote,
|
||||||
|
KeyMetrics,
|
||||||
|
MainForceInfo,
|
||||||
|
CompanyInfo,
|
||||||
|
StockCompareModal,
|
||||||
|
STOCK_CARD_THEME,
|
||||||
|
} from './components';
|
||||||
import type { StockQuoteCardProps } from './types';
|
import type { StockQuoteCardProps } from './types';
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化价格显示
|
|
||||||
*/
|
|
||||||
const formatPrice = (price: number): string => {
|
|
||||||
return price.toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化涨跌幅显示
|
|
||||||
*/
|
|
||||||
const formatChangePercent = (percent: number): string => {
|
|
||||||
const sign = percent >= 0 ? '+' : '';
|
|
||||||
return `${sign}${percent.toFixed(2)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化主力净流入显示
|
|
||||||
*/
|
|
||||||
const formatNetInflow = (value: number): string => {
|
|
||||||
const sign = value >= 0 ? '+' : '';
|
|
||||||
return `${sign}${value.toFixed(2)}亿`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
||||||
data,
|
data,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -74,11 +46,6 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
// 对比弹窗控制
|
// 对比弹窗控制
|
||||||
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
|
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
|
||||||
|
|
||||||
// 处理分享点击
|
|
||||||
const handleShare = () => {
|
|
||||||
onShare?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理对比按钮点击
|
// 处理对比按钮点击
|
||||||
const handleCompare = (stockCode: string) => {
|
const handleCompare = (stockCode: string) => {
|
||||||
onCompare?.(stockCode);
|
onCompare?.(stockCode);
|
||||||
@@ -91,16 +58,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
onCloseCompare?.();
|
onCloseCompare?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 黑金主题颜色配置
|
const { cardBg, borderColor } = STOCK_CARD_THEME;
|
||||||
const cardBg = '#1A202C';
|
|
||||||
const borderColor = '#C9A961';
|
|
||||||
const labelColor = '#C9A961';
|
|
||||||
const valueColor = '#F4D03F';
|
|
||||||
const sectionTitleColor = '#F4D03F';
|
|
||||||
|
|
||||||
// 涨跌颜色(红涨绿跌)
|
|
||||||
const upColor = '#F44336'; // 涨 - 红色
|
|
||||||
const downColor = '#4CAF50'; // 跌 - 绿色
|
|
||||||
|
|
||||||
// 加载中或无数据时显示骨架屏
|
// 加载中或无数据时显示骨架屏
|
||||||
if (isLoading || !data) {
|
if (isLoading || !data) {
|
||||||
@@ -117,82 +75,24 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceColor = data.changePercent >= 0 ? upColor : downColor;
|
|
||||||
const inflowColor = data.mainNetInflow >= 0 ? upColor : downColor;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
|
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
|
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
|
||||||
<Flex justify="space-between" align="center" mb={4}>
|
<StockHeader
|
||||||
{/* 左侧:股票名称 + 行业标签 + 指数标签 */}
|
name={data.name}
|
||||||
<HStack spacing={3} align="center">
|
code={data.code}
|
||||||
{/* 股票名称 - 突出显示 */}
|
industryL1={data.industryL1}
|
||||||
<Text fontSize="26px" fontWeight="800" color={valueColor}>
|
industry={data.industry}
|
||||||
{data.name}
|
indexTags={data.indexTags}
|
||||||
</Text>
|
updateTime={data.updateTime}
|
||||||
<Text fontSize="18px" fontWeight="normal" color={labelColor}>
|
isInWatchlist={isInWatchlist}
|
||||||
({data.code})
|
isWatchlistLoading={isWatchlistLoading}
|
||||||
</Text>
|
onWatchlistToggle={onWatchlistToggle}
|
||||||
|
onShare={onShare}
|
||||||
{/* 行业标签 */}
|
isCompareLoading={isCompareLoading}
|
||||||
{(data.industryL1 || data.industry) && (
|
|
||||||
<Badge
|
|
||||||
bg="transparent"
|
|
||||||
color={labelColor}
|
|
||||||
fontSize="14px"
|
|
||||||
fontWeight="medium"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor={borderColor}
|
|
||||||
px={2}
|
|
||||||
py={0.5}
|
|
||||||
borderRadius="md"
|
|
||||||
>
|
|
||||||
{data.industryL1 && data.industry
|
|
||||||
? `${data.industryL1} · ${data.industry}`
|
|
||||||
: data.industry || data.industryL1}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 指数标签 */}
|
|
||||||
{data.indexTags?.length > 0 && (
|
|
||||||
<Text fontSize="14px" color={labelColor}>
|
|
||||||
{data.indexTags.join('、')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 右侧:对比 + 关注 + 分享 + 时间 */}
|
|
||||||
<HStack spacing={3}>
|
|
||||||
{/* 股票对比输入 */}
|
|
||||||
<CompareStockInput
|
|
||||||
onCompare={handleCompare}
|
onCompare={handleCompare}
|
||||||
isLoading={isCompareLoading}
|
|
||||||
currentStockCode={data.code}
|
|
||||||
/>
|
/>
|
||||||
<FavoriteButton
|
|
||||||
isFavorite={isInWatchlist}
|
|
||||||
isLoading={isWatchlistLoading}
|
|
||||||
onClick={onWatchlistToggle || (() => {})}
|
|
||||||
colorScheme="gold"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Tooltip label="分享" placement="top">
|
|
||||||
<IconButton
|
|
||||||
aria-label="分享"
|
|
||||||
icon={<Share2 size={18} />}
|
|
||||||
variant="ghost"
|
|
||||||
color={labelColor}
|
|
||||||
size="sm"
|
|
||||||
onClick={handleShare}
|
|
||||||
_hover={{ bg: 'whiteAlpha.100' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Text fontSize="14px" color={labelColor}>
|
|
||||||
{data.updateTime?.split(' ')[1] || '--:--'}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* 股票对比弹窗 */}
|
{/* 股票对比弹窗 */}
|
||||||
<StockCompareModal
|
<StockCompareModal
|
||||||
@@ -209,202 +109,39 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
<Flex gap={8}>
|
<Flex gap={8}>
|
||||||
{/* 左栏:价格信息 (flex=1) */}
|
{/* 左栏:价格信息 (flex=1) */}
|
||||||
<Box flex="1" minWidth="0">
|
<Box flex="1" minWidth="0">
|
||||||
<HStack align="baseline" spacing={3} mb={3}>
|
<PriceDisplay
|
||||||
<Text fontSize="48px" fontWeight="bold" color={priceColor}>
|
currentPrice={data.currentPrice}
|
||||||
{formatPrice(data.currentPrice)}
|
changePercent={data.changePercent}
|
||||||
</Text>
|
/>
|
||||||
<Badge
|
<SecondaryQuote
|
||||||
bg={data.changePercent >= 0 ? upColor : downColor}
|
todayOpen={data.todayOpen}
|
||||||
color="#FFFFFF"
|
yesterdayClose={data.yesterdayClose}
|
||||||
fontSize="20px"
|
todayHigh={data.todayHigh}
|
||||||
fontWeight="bold"
|
todayLow={data.todayLow}
|
||||||
px={3}
|
/>
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
>
|
|
||||||
{formatChangePercent(data.changePercent)}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
{/* 次要行情:今开 | 昨收 | 最高 | 最低 */}
|
|
||||||
<HStack spacing={4} fontSize="14px" flexWrap="wrap">
|
|
||||||
<Text color={labelColor}>
|
|
||||||
今开:
|
|
||||||
<Text as="span" color={valueColor} fontWeight="bold">
|
|
||||||
{formatPrice(data.todayOpen)}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Text color={borderColor}>|</Text>
|
|
||||||
<Text color={labelColor}>
|
|
||||||
昨收:
|
|
||||||
<Text as="span" color={valueColor} fontWeight="bold">
|
|
||||||
{formatPrice(data.yesterdayClose)}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Text color={borderColor}>|</Text>
|
|
||||||
<Text color={labelColor}>
|
|
||||||
最高:
|
|
||||||
<Text as="span" color={upColor} fontWeight="bold">
|
|
||||||
{formatPrice(data.todayHigh)}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Text color={borderColor}>|</Text>
|
|
||||||
<Text color={labelColor}>
|
|
||||||
最低:
|
|
||||||
<Text as="span" color={downColor} fontWeight="bold">
|
|
||||||
{formatPrice(data.todayLow)}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 右栏:关键指标 + 主力动态 (flex=2) */}
|
{/* 右栏:关键指标 + 主力动态 (flex=2) */}
|
||||||
<Flex flex="2" minWidth="0" gap={8} borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
<Flex flex="2" minWidth="0" gap={8} borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
||||||
{/* 关键指标 */}
|
<KeyMetrics
|
||||||
<Box flex="1">
|
pe={data.pe}
|
||||||
<Text
|
eps={data.eps}
|
||||||
fontSize="14px"
|
pb={data.pb}
|
||||||
fontWeight="bold"
|
marketCap={data.marketCap}
|
||||||
color={sectionTitleColor}
|
week52Low={data.week52Low}
|
||||||
mb={3}
|
week52High={data.week52High}
|
||||||
>
|
/>
|
||||||
关键指标
|
<MainForceInfo
|
||||||
</Text>
|
mainNetInflow={data.mainNetInflow}
|
||||||
<VStack align="stretch" spacing={2} fontSize="14px">
|
institutionHolding={data.institutionHolding}
|
||||||
<HStack justify="space-between">
|
buyRatio={data.buyRatio}
|
||||||
<Text color={labelColor}>市盈率(PE):</Text>
|
sellRatio={data.sellRatio}
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{data.pe.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>每股收益(EPS):</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{data.eps?.toFixed(3) || '-'}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>市净率(PB):</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{data.pb.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>流通市值:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{data.marketCap}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>52周波动:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{formatPrice(data.week52Low)}-{formatPrice(data.week52High)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 主力动态 */}
|
|
||||||
<Box flex="1" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
|
||||||
<Text
|
|
||||||
fontSize="14px"
|
|
||||||
fontWeight="bold"
|
|
||||||
color={sectionTitleColor}
|
|
||||||
mb={3}
|
|
||||||
>
|
|
||||||
主力动态
|
|
||||||
</Text>
|
|
||||||
<VStack align="stretch" spacing={2} fontSize="14px">
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>主力净流入:</Text>
|
|
||||||
<Text color={inflowColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{formatNetInflow(data.mainNetInflow)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text color={labelColor}>机构持仓:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold" fontSize="16px">
|
|
||||||
{data.institutionHolding.toFixed(2)}%
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
{/* 买卖比例条 */}
|
|
||||||
<Box mt={1}>
|
|
||||||
<Progress
|
|
||||||
value={data.buyRatio}
|
|
||||||
size="sm"
|
|
||||||
sx={{
|
|
||||||
'& > div': { bg: upColor },
|
|
||||||
}}
|
|
||||||
bg={downColor}
|
|
||||||
borderRadius="full"
|
|
||||||
/>
|
/>
|
||||||
<HStack justify="space-between" mt={1} fontSize="14px">
|
|
||||||
<Text color={upColor}>买入{data.buyRatio}%</Text>
|
|
||||||
<Text color={downColor}>卖出{data.sellRatio}%</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 公司信息区块 - 1:2 布局 */}
|
{/* 公司信息区块 */}
|
||||||
{basicInfo && (
|
{basicInfo && <CompanyInfo basicInfo={basicInfo} />}
|
||||||
<>
|
|
||||||
<Divider borderColor={borderColor} my={4} />
|
|
||||||
<Flex gap={8}>
|
|
||||||
{/* 左侧:公司关键属性 (flex=1) */}
|
|
||||||
<Box flex="1" minWidth="0">
|
|
||||||
<HStack spacing={4} flexWrap="wrap" fontSize="14px">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={Calendar} color={labelColor} boxSize={4} />
|
|
||||||
<Text color={labelColor}>成立:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold">
|
|
||||||
{formatDate(basicInfo.establish_date)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={Coins} color={labelColor} boxSize={4} />
|
|
||||||
<Text color={labelColor}>注册资本:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold">
|
|
||||||
{formatRegisteredCapital(basicInfo.reg_capital)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={MapPin} color={labelColor} boxSize={4} />
|
|
||||||
<Text color={labelColor}>所在地:</Text>
|
|
||||||
<Text color={valueColor} fontWeight="bold">
|
|
||||||
{basicInfo.province} {basicInfo.city}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={Globe} color={labelColor} boxSize={4} />
|
|
||||||
{basicInfo.website ? (
|
|
||||||
<Link
|
|
||||||
href={basicInfo.website}
|
|
||||||
isExternal
|
|
||||||
color={valueColor}
|
|
||||||
fontWeight="bold"
|
|
||||||
_hover={{ color: labelColor }}
|
|
||||||
>
|
|
||||||
访问官网
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Text color={valueColor}>暂无官网</Text>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 右侧:公司简介 (flex=2) */}
|
|
||||||
<Box flex="2" minWidth="0" borderLeftWidth="1px" borderColor={borderColor} pl={8}>
|
|
||||||
<Text fontSize="14px" color={labelColor} noOfLines={2}>
|
|
||||||
<Text as="span" fontWeight="bold" color={valueColor}>公司简介:</Text>
|
|
||||||
{basicInfo.company_intro || '暂无'}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user