refactor(StockQuoteCard): 拆分为原子组件

- 新增 theme.ts 黑金主题常量
  - 新增 formatters.ts 格式化工具函数
  - 拆分 PriceDisplay/SecondaryQuote/KeyMetrics/MainForceInfo/CompanyInfo/StockHeader
  - 主组件从 414 行简化为 150 行
  - 提高可维护性和复用性
This commit is contained in:
zdl
2025-12-16 20:24:01 +08:00
parent 84914b3cca
commit 3bd48e1ddd
10 changed files with 579 additions and 316 deletions

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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)}亿`;
};

View File

@@ -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 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';

View File

@@ -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;

View File

@@ -2,6 +2,7 @@
* StockQuoteCard - 股票行情卡片组件
*
* 展示股票的实时行情、关键指标和主力动态
* 采用原子组件拆分,提高可维护性和复用性
*/
import React from 'react';
@@ -10,52 +11,23 @@ import {
Card,
CardBody,
Flex,
HStack,
VStack,
Text,
Badge,
Progress,
Skeleton,
IconButton,
Tooltip,
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 {
StockHeader,
PriceDisplay,
SecondaryQuote,
KeyMetrics,
MainForceInfo,
CompanyInfo,
StockCompareModal,
STOCK_CARD_THEME,
} from './components';
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> = ({
data,
isLoading = false,
@@ -74,11 +46,6 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
// 对比弹窗控制
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
// 处理分享点击
const handleShare = () => {
onShare?.();
};
// 处理对比按钮点击
const handleCompare = (stockCode: string) => {
onCompare?.(stockCode);
@@ -91,16 +58,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
onCloseCompare?.();
};
// 黑金主题颜色配置
const cardBg = '#1A202C';
const borderColor = '#C9A961';
const labelColor = '#C9A961';
const valueColor = '#F4D03F';
const sectionTitleColor = '#F4D03F';
// 涨跌颜色(红涨绿跌)
const upColor = '#F44336'; // 涨 - 红色
const downColor = '#4CAF50'; // 跌 - 绿色
const { cardBg, borderColor } = STOCK_CARD_THEME;
// 加载中或无数据时显示骨架屏
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 (
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
<CardBody>
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
<Flex justify="space-between" align="center" mb={4}>
{/* 左侧:股票名称 + 行业标签 + 指数标签 */}
<HStack spacing={3} align="center">
{/* 股票名称 - 突出显示 */}
<Text fontSize="26px" fontWeight="800" color={valueColor}>
{data.name}
</Text>
<Text fontSize="18px" fontWeight="normal" color={labelColor}>
({data.code})
</Text>
{/* 行业标签 */}
{(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
<StockHeader
name={data.name}
code={data.code}
industryL1={data.industryL1}
industry={data.industry}
indexTags={data.indexTags}
updateTime={data.updateTime}
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={onWatchlistToggle}
onShare={onShare}
isCompareLoading={isCompareLoading}
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
@@ -209,202 +109,39 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
<Flex gap={8}>
{/* 左栏:价格信息 (flex=1) */}
<Box flex="1" minWidth="0">
<HStack align="baseline" spacing={3} mb={3}>
<Text fontSize="48px" fontWeight="bold" color={priceColor}>
{formatPrice(data.currentPrice)}
</Text>
<Badge
bg={data.changePercent >= 0 ? upColor : downColor}
color="#FFFFFF"
fontSize="20px"
fontWeight="bold"
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>
<PriceDisplay
currentPrice={data.currentPrice}
changePercent={data.changePercent}
/>
<SecondaryQuote
todayOpen={data.todayOpen}
yesterdayClose={data.yesterdayClose}
todayHigh={data.todayHigh}
todayLow={data.todayLow}
/>
</Box>
{/* 右栏:关键指标 + 主力动态 (flex=2) */}
<Flex flex="2" minWidth="0" gap={8} borderLeftWidth="1px" borderColor={borderColor} pl={8}>
{/* 关键指标 */}
<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">
{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"
<KeyMetrics
pe={data.pe}
eps={data.eps}
pb={data.pb}
marketCap={data.marketCap}
week52Low={data.week52Low}
week52High={data.week52High}
/>
<MainForceInfo
mainNetInflow={data.mainNetInflow}
institutionHolding={data.institutionHolding}
buyRatio={data.buyRatio}
sellRatio={data.sellRatio}
/>
<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>
{/* 公司信息区块 - 1:2 布局 */}
{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>
</>
)}
{/* 公司信息区块 */}
{basicInfo && <CompanyInfo basicInfo={basicInfo} />}
</CardBody>
</Card>
);