- 将公司基本信息整合到 StockQuoteCard 内部 - 采用 1:2 Flex 布局确保上下竖线对齐 - 删除废弃的 CompanyHeaderCard 组件 - 清理 types.ts 中的 CompanyHeaderCardProps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
/**
|
||
* StockQuoteCard - 股票行情卡片组件
|
||
*
|
||
* 展示股票的实时行情、关键指标和主力动态
|
||
*/
|
||
|
||
import React from 'react';
|
||
import {
|
||
Box,
|
||
Card,
|
||
CardBody,
|
||
Flex,
|
||
HStack,
|
||
VStack,
|
||
Text,
|
||
Badge,
|
||
Progress,
|
||
Skeleton,
|
||
IconButton,
|
||
Tooltip,
|
||
Divider,
|
||
Link,
|
||
Icon,
|
||
} 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 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,
|
||
isInWatchlist = false,
|
||
isWatchlistLoading = false,
|
||
onWatchlistToggle,
|
||
onShare,
|
||
basicInfo,
|
||
}) => {
|
||
// 处理分享点击
|
||
const handleShare = () => {
|
||
onShare?.();
|
||
};
|
||
|
||
// 黑金主题颜色配置
|
||
const cardBg = '#1A202C';
|
||
const borderColor = '#C9A961';
|
||
const labelColor = '#C9A961';
|
||
const valueColor = '#F4D03F';
|
||
const sectionTitleColor = '#F4D03F';
|
||
|
||
// 涨跌颜色(红涨绿跌)
|
||
const upColor = '#F44336'; // 涨 - 红色
|
||
const downColor = '#4CAF50'; // 跌 - 绿色
|
||
|
||
// 加载中或无数据时显示骨架屏
|
||
if (isLoading || !data) {
|
||
return (
|
||
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
|
||
<CardBody>
|
||
<VStack spacing={4} align="stretch">
|
||
<Skeleton height="30px" width="200px" />
|
||
<Skeleton height="60px" />
|
||
<Skeleton height="80px" />
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
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}>
|
||
<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>
|
||
|
||
{/* 1:2 布局 */}
|
||
<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>
|
||
</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}>市净率(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>
|
||
|
||
{/* 公司信息区块 - 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>
|
||
</>
|
||
)}
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default StockQuoteCard;
|