refactor(StockSummaryCard): 黑金主题 4 列布局重构

- 布局从 1+3 改为 4 列横向排列(股票信息/交易热度/估值安全/情绪风险)
- 新增 darkGoldTheme 黑金主题配置
- 采用原子设计模式拆分:5 个原子组件 + 2 个业务组件
- 原子组件:DarkGoldCard、CardTitle、MetricValue、PriceDisplay、StatusTag
- 业务组件:StockHeaderCard、MetricCard
- 提取状态计算工具到 utils.ts
- types.ts: theme 参数改为可选

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-16 14:01:42 +08:00
parent 276b280cb9
commit 09ca7265d7
13 changed files with 565 additions and 134 deletions

View File

@@ -1,133 +0,0 @@
// src/views/Company/components/MarketDataView/components/StockSummaryCard.tsx
// 股票概览卡片组件
import React from 'react';
import {
CardBody,
Grid,
GridItem,
VStack,
HStack,
Heading,
Badge,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
SimpleGrid,
} from '@chakra-ui/react';
import ThemedCard from './ThemedCard';
import { formatNumber, formatPercent } from '../utils/formatUtils';
import type { StockSummaryCardProps } from '../types';
/**
* 股票概览卡片组件
* 显示股票基本信息、最新交易数据和融资融券数据
*/
const StockSummaryCard: React.FC<StockSummaryCardProps> = ({ summary, theme }) => {
if (!summary) return null;
const { latest_trade, latest_funding, latest_pledge } = summary;
return (
<ThemedCard theme={theme}>
<CardBody>
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
{/* 左侧:股票名称和涨跌 */}
<GridItem colSpan={{ base: 12, md: 4 }}>
<VStack align="start" spacing={2}>
<HStack>
<Heading size="xl" color={theme.textSecondary}>
{summary.stock_name}
</Heading>
<Badge colorScheme="blue" fontSize="lg">
{summary.stock_code}
</Badge>
</HStack>
{latest_trade && (
<HStack spacing={4}>
<Stat>
<StatNumber fontSize="4xl" color={theme.textPrimary}>
{latest_trade.close}
</StatNumber>
<StatHelpText fontSize="lg">
<StatArrow
type={latest_trade.change_percent >= 0 ? 'increase' : 'decrease'}
color={latest_trade.change_percent >= 0 ? theme.success : theme.danger}
/>
{Math.abs(latest_trade.change_percent).toFixed(2)}%
</StatHelpText>
</Stat>
</HStack>
)}
</VStack>
</GridItem>
{/* 右侧:详细指标 */}
<GridItem colSpan={{ base: 12, md: 8 }}>
{/* 交易指标 */}
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
{latest_trade && (
<>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.textSecondary}>
{formatNumber(latest_trade.volume, 0)}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.textSecondary}>
{formatNumber(latest_trade.amount)}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.textSecondary}>
{formatPercent(latest_trade.turnover_rate)}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.textSecondary}>
{latest_trade.pe_ratio || '-'}
</StatNumber>
</Stat>
</>
)}
</SimpleGrid>
{/* 融资融券和质押指标 */}
{latest_funding && (
<SimpleGrid columns={{ base: 2, md: 3 }} spacing={4} mt={4}>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.success} fontSize="lg">
{formatNumber(latest_funding.financing_balance)}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.danger} fontSize="lg">
{formatNumber(latest_funding.securities_balance)}
</StatNumber>
</Stat>
{latest_pledge && (
<Stat>
<StatLabel color={theme.textMuted}></StatLabel>
<StatNumber color={theme.warning} fontSize="lg">
{formatPercent(latest_pledge.pledge_ratio)}
</StatNumber>
</Stat>
)}
</SimpleGrid>
)}
</GridItem>
</Grid>
</CardBody>
</ThemedCard>
);
};
export default StockSummaryCard;

View File

@@ -0,0 +1,56 @@
// 指标卡片组件
import React from 'react';
import { Box, VStack } from '@chakra-ui/react';
import { DarkGoldCard, CardTitle, MetricValue } from './atoms';
import { darkGoldTheme } from '../../constants';
export interface MetricCardProps {
title: string;
subtitle: string;
leftIcon: React.ReactNode;
rightIcon?: React.ReactNode;
mainLabel: string;
mainValue: string;
mainColor: string;
mainSuffix?: string;
subText: React.ReactNode;
}
/**
* 指标卡片组件 - 用于展示单个指标数据
*/
const MetricCard: React.FC<MetricCardProps> = ({
title,
subtitle,
leftIcon,
rightIcon,
mainLabel,
mainValue,
mainColor,
mainSuffix,
subText,
}) => (
<DarkGoldCard>
<CardTitle
title={title}
subtitle={subtitle}
leftIcon={leftIcon}
rightIcon={rightIcon}
/>
<VStack align="start" spacing={1} mb={3}>
<MetricValue
label={mainLabel}
value={mainValue}
color={mainColor}
suffix={mainSuffix}
/>
</VStack>
<Box color={darkGoldTheme.textMuted} fontSize="sm">
{subText}
</Box>
</DarkGoldCard>
);
export default MetricCard;

View File

@@ -0,0 +1,90 @@
// 股票信息卡片组件4列布局版本
import React from 'react';
import { Box, HStack, Text, Icon } from '@chakra-ui/react';
import { TrendingUp, TrendingDown } from 'lucide-react';
import { DarkGoldCard } from './atoms';
import { getTrendDescription, getPriceColor } from './utils';
import { darkGoldTheme } from '../../constants';
export interface StockHeaderCardProps {
stockName: string;
stockCode: string;
price: number;
changePercent: number;
}
/**
* 股票信息卡片 - 4 列布局中的第一个卡片
*/
const StockHeaderCard: React.FC<StockHeaderCardProps> = ({
stockName,
stockCode,
price,
changePercent,
}) => {
const isUp = changePercent >= 0;
const priceColor = getPriceColor(changePercent);
const trendDesc = getTrendDescription(changePercent);
return (
<DarkGoldCard position="relative" overflow="hidden">
{/* 背景装饰线 */}
<Box
position="absolute"
right={0}
top={0}
width="60%"
height="100%"
opacity={0.12}
background={`linear-gradient(135deg, transparent 30%, ${priceColor})`}
clipPath="polygon(40% 0, 100% 0, 100% 100%, 20% 100%)"
/>
{/* 股票名称和代码 */}
<HStack spacing={2} mb={3}>
<Text
color={darkGoldTheme.textPrimary}
fontSize="xl"
fontWeight="bold"
>
{stockName}
</Text>
<Text color={darkGoldTheme.textMuted} fontSize="md">
({stockCode})
</Text>
</HStack>
{/* 价格和涨跌幅 */}
<HStack spacing={3} align="baseline" mb={2}>
<Text
color={priceColor}
fontSize="4xl"
fontWeight="bold"
lineHeight="1"
>
{price.toFixed(2)}
</Text>
<HStack spacing={1} align="center">
<Icon
as={isUp ? TrendingUp : TrendingDown}
color={priceColor}
boxSize={5}
/>
<Text color={priceColor} fontSize="lg" fontWeight="bold">
{isUp ? '+' : ''}{changePercent.toFixed(2)}%
</Text>
</HStack>
</HStack>
{/* 走势简述 */}
<Text color={darkGoldTheme.textMuted} fontSize="sm">
<Text as="span" color={priceColor} fontWeight="medium">
{trendDesc}
</Text>
</Text>
</DarkGoldCard>
);
};
export default StockHeaderCard;

View File

@@ -0,0 +1,36 @@
// 卡片标题原子组件
import React from 'react';
import { Flex, HStack, Box, Text } from '@chakra-ui/react';
import { darkGoldTheme } from '../../../constants';
interface CardTitleProps {
title: string;
subtitle: string;
leftIcon: React.ReactNode;
rightIcon?: React.ReactNode;
}
/**
* 卡片标题组件 - 显示图标+标题+副标题
*/
const CardTitle: React.FC<CardTitleProps> = ({
title,
subtitle,
leftIcon,
rightIcon,
}) => (
<Flex justify="space-between" align="center" mb={4}>
<HStack spacing={2}>
<Box color={darkGoldTheme.gold}>{leftIcon}</Box>
<Text color={darkGoldTheme.gold} fontSize="lg" fontWeight="bold">
{title}
</Text>
<Text color={darkGoldTheme.textMuted} fontSize="sm">
({subtitle})
</Text>
</HStack>
{rightIcon && <Box color={darkGoldTheme.gold}>{rightIcon}</Box>}
</Flex>
);
export default CardTitle;

View File

@@ -0,0 +1,42 @@
// 黑金主题卡片容器原子组件
import React from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { darkGoldTheme } from '../../../constants';
interface DarkGoldCardProps extends BoxProps {
children: React.ReactNode;
hoverable?: boolean;
}
/**
* 黑金主题卡片容器
*/
const DarkGoldCard: React.FC<DarkGoldCardProps> = ({
children,
hoverable = true,
...props
}) => (
<Box
bg={darkGoldTheme.bgCard}
borderRadius="xl"
border="1px solid"
borderColor={darkGoldTheme.border}
p={5}
transition="all 0.3s ease"
_hover={
hoverable
? {
bg: darkGoldTheme.bgCardHover,
borderColor: darkGoldTheme.borderHover,
transform: 'translateY(-2px)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
}
: undefined
}
{...props}
>
{children}
</Box>
);
export default DarkGoldCard;

View File

@@ -0,0 +1,54 @@
// 核心数值展示原子组件
import React from 'react';
import { HStack, Text } from '@chakra-ui/react';
import { darkGoldTheme } from '../../../constants';
interface MetricValueProps {
label: string;
value: string;
color: string;
suffix?: string;
size?: 'sm' | 'md' | 'lg';
}
const sizeMap = {
sm: { label: 'sm', value: '2xl', suffix: 'md' },
md: { label: 'md', value: '3xl', suffix: 'lg' },
lg: { label: 'md', value: '4xl', suffix: 'xl' },
};
/**
* 核心数值展示组件 - 显示标签+数值
*/
const MetricValue: React.FC<MetricValueProps> = ({
label,
value,
color,
suffix,
size = 'lg',
}) => {
const sizes = sizeMap[size];
return (
<HStack spacing={2} align="baseline">
<Text color={darkGoldTheme.textMuted} fontSize={sizes.label}>
{label}
</Text>
<Text
color={color}
fontSize={sizes.value}
fontWeight="bold"
lineHeight="1"
>
{value}
</Text>
{suffix && (
<Text color={color} fontSize={sizes.suffix} fontWeight="bold">
{suffix}
</Text>
)}
</HStack>
);
};
export default MetricValue;

View File

@@ -0,0 +1,56 @@
// 价格显示原子组件
import React from 'react';
import { HStack, Text, Icon } from '@chakra-ui/react';
import { TrendingUp, TrendingDown } from 'lucide-react';
interface PriceDisplayProps {
price: number;
changePercent: number;
priceColor: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const sizeMap = {
sm: { price: '2xl', percent: 'md', icon: 4 },
md: { price: '3xl', percent: 'lg', icon: 5 },
lg: { price: '4xl', percent: 'xl', icon: 6 },
xl: { price: '5xl', percent: 'xl', icon: 6 },
};
/**
* 价格显示组件 - 显示价格和涨跌幅
*/
const PriceDisplay: React.FC<PriceDisplayProps> = ({
price,
changePercent,
priceColor,
size = 'xl',
}) => {
const isUp = changePercent >= 0;
const sizes = sizeMap[size];
return (
<HStack spacing={4} align="baseline">
<Text
color={priceColor}
fontSize={sizes.price}
fontWeight="bold"
lineHeight="1"
>
{price.toFixed(2)}
</Text>
<HStack spacing={1} align="center">
<Icon
as={isUp ? TrendingUp : TrendingDown}
color={priceColor}
boxSize={sizes.icon}
/>
<Text color={priceColor} fontSize={sizes.percent} fontWeight="bold">
{isUp ? '+' : ''}{changePercent.toFixed(2)}%
</Text>
</HStack>
</HStack>
);
};
export default PriceDisplay;

View File

@@ -0,0 +1,24 @@
// 状态标签原子组件
import React from 'react';
import { Text } from '@chakra-ui/react';
interface StatusTagProps {
text: string;
color: string;
showParentheses?: boolean;
}
/**
* 状态标签 - 显示如"活跃"、"健康"等状态文字
*/
const StatusTag: React.FC<StatusTagProps> = ({
text,
color,
showParentheses = true,
}) => (
<Text color={color} fontWeight="medium" ml={1}>
{showParentheses ? `(${text})` : text}
</Text>
);
export default StatusTag;

View File

@@ -0,0 +1,6 @@
// 原子组件统一导出
export { default as StatusTag } from './StatusTag';
export { default as PriceDisplay } from './PriceDisplay';
export { default as MetricValue } from './MetricValue';
export { default as CardTitle } from './CardTitle';
export { default as DarkGoldCard } from './DarkGoldCard';

View File

@@ -0,0 +1,114 @@
// StockSummaryCard 主组件
import React from 'react';
import { SimpleGrid, HStack, Text, VStack } from '@chakra-ui/react';
import { Flame, Coins, DollarSign, Shield } from 'lucide-react';
import StockHeaderCard from './StockHeaderCard';
import MetricCard from './MetricCard';
import { StatusTag } from './atoms';
import { getTurnoverStatus, getPEStatus, getPledgeStatus } from './utils';
import { formatNumber, formatPercent } from '../../utils/formatUtils';
import { darkGoldTheme } from '../../constants';
import type { StockSummaryCardProps } from '../../types';
/**
* 股票概览卡片组件
* 4 列横向布局:股票信息 + 交易热度 + 估值安全 + 情绪风险
*/
const StockSummaryCard: React.FC<StockSummaryCardProps> = ({ summary }) => {
if (!summary) return null;
const { latest_trade, latest_funding, latest_pledge } = summary;
// 计算状态
const turnoverStatus = latest_trade
? getTurnoverStatus(latest_trade.turnover_rate)
: { text: '-', color: darkGoldTheme.textMuted };
const peStatus = getPEStatus(latest_trade?.pe_ratio);
const pledgeStatus = latest_pledge
? getPledgeStatus(latest_pledge.pledge_ratio)
: { text: '-', color: darkGoldTheme.textMuted };
return (
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={4}>
{/* 卡片1: 股票信息 */}
{latest_trade && (
<StockHeaderCard
stockName={summary.stock_name}
stockCode={summary.stock_code}
price={latest_trade.close}
changePercent={latest_trade.change_percent}
/>
)}
{/* 卡片1: 交易热度 */}
<MetricCard
title="交易热度"
subtitle="流动性"
leftIcon={<Flame size={22} />}
rightIcon={<Coins size={22} />}
mainLabel="成交额"
mainValue={latest_trade ? formatNumber(latest_trade.amount) : '-'}
mainColor={darkGoldTheme.orange}
subText={
<HStack spacing={1} flexWrap="wrap">
<Text>
{latest_trade ? formatNumber(latest_trade.volume, 0) : '-'}
</Text>
<Text>|</Text>
<Text>
{latest_trade ? formatPercent(latest_trade.turnover_rate) : '-'}
</Text>
<StatusTag text={turnoverStatus.text} color={turnoverStatus.color} />
</HStack>
}
/>
{/* 卡片2: 估值 VS 安全 */}
<MetricCard
title="估值 VS 安全"
subtitle="便宜否"
leftIcon={<DollarSign size={22} />}
rightIcon={<Shield size={22} />}
mainLabel="市盈率(PE)"
mainValue={latest_trade?.pe_ratio?.toFixed(2) || '-'}
mainColor={darkGoldTheme.orange}
subText={
<VStack align="start" spacing={1}>
<Text color={peStatus.color} fontWeight="medium">
{peStatus.text}
</Text>
<HStack spacing={1} flexWrap="wrap">
<Text>
{latest_pledge ? formatPercent(latest_pledge.pledge_ratio) : '-'}
</Text>
<StatusTag text={pledgeStatus.text} color={pledgeStatus.color} />
</HStack>
</VStack>
}
/>
{/* 卡片3: 情绪与风险 */}
<MetricCard
title="情绪与风险"
subtitle="资金面"
leftIcon={<Flame size={22} />}
mainLabel="融资余额"
mainValue={latest_funding ? formatNumber(latest_funding.financing_balance) : '-'}
mainColor={darkGoldTheme.green}
subText={
<VStack align="start" spacing={0}>
<Text color={darkGoldTheme.textMuted}>()</Text>
<HStack spacing={1} flexWrap="wrap" mt={1}>
<Text>
{latest_funding ? formatNumber(latest_funding.securities_balance) : '-'}
</Text>
</HStack>
</VStack>
}
/>
</SimpleGrid>
);
};
export default StockSummaryCard;

View File

@@ -0,0 +1,57 @@
// 状态计算工具函数
import { darkGoldTheme } from '../../constants';
export interface StatusResult {
text: string;
color: string;
}
/**
* 获取走势简述
*/
export const getTrendDescription = (changePercent: number): string => {
if (changePercent >= 5) return '强势上涨';
if (changePercent >= 2) return '稳步上涨';
if (changePercent > 0) return '小幅上涨';
if (changePercent === 0) return '横盘整理';
if (changePercent > -2) return '小幅下跌';
if (changePercent > -5) return '震荡下跌';
return '大幅下跌';
};
/**
* 获取换手率状态标签
*/
export const getTurnoverStatus = (rate: number): StatusResult => {
if (rate >= 3) return { text: '活跃', color: darkGoldTheme.orange };
if (rate >= 1) return { text: '正常', color: darkGoldTheme.gold };
return { text: '冷清', color: darkGoldTheme.textMuted };
};
/**
* 获取市盈率估值标签
*/
export const getPEStatus = (pe: number | undefined): StatusResult => {
if (!pe || pe <= 0) return { text: '亏损', color: darkGoldTheme.red };
if (pe < 10) return { text: '极低估值 / 安全边际高', color: darkGoldTheme.green };
if (pe < 20) return { text: '合理估值', color: darkGoldTheme.gold };
if (pe < 40) return { text: '偏高估值', color: darkGoldTheme.orange };
return { text: '高估值 / 泡沫风险', color: darkGoldTheme.red };
};
/**
* 获取质押率健康状态
*/
export const getPledgeStatus = (ratio: number): StatusResult => {
if (ratio < 10) return { text: '健康', color: darkGoldTheme.green };
if (ratio < 30) return { text: '正常', color: darkGoldTheme.gold };
if (ratio < 50) return { text: '偏高', color: darkGoldTheme.orange };
return { text: '警惕', color: darkGoldTheme.red };
};
/**
* 获取价格颜色
*/
export const getPriceColor = (changePercent: number): string => {
return changePercent >= 0 ? darkGoldTheme.red : darkGoldTheme.green;
};

View File

@@ -28,6 +28,35 @@ export const themes: Record<'light', Theme> = {
}, },
}; };
/**
* 黑金主题配置 - 用于 StockSummaryCard
*/
export const darkGoldTheme = {
// 背景
bgCard: 'linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%)',
bgCardHover: 'linear-gradient(135deg, #252540 0%, #1a1a2e 100%)',
// 边框
border: 'rgba(212, 175, 55, 0.3)',
borderHover: 'rgba(212, 175, 55, 0.6)',
// 文字
textPrimary: '#FFFFFF',
textSecondary: 'rgba(255, 255, 255, 0.85)',
textMuted: 'rgba(255, 255, 255, 0.6)',
// 强调色
gold: '#D4AF37',
goldLight: '#F4D03F',
orange: '#FF9500',
green: '#00C851',
red: '#FF4444',
// 标签背景
tagBg: 'rgba(212, 175, 55, 0.15)',
tagText: '#D4AF37',
};
/** /**
* 默认股票代码 * 默认股票代码
*/ */

View File

@@ -270,7 +270,7 @@ export interface MarkdownRendererProps {
*/ */
export interface StockSummaryCardProps { export interface StockSummaryCardProps {
summary: MarketSummary; summary: MarketSummary;
theme: Theme; theme?: Theme; // 可选StockSummaryCard 使用内置黑金主题
} }
/** /**