refactor(StockQuoteCard): 组件拆分与 FUI 光效统一

- 新增 CardGlow 组件到 @components/FUI,支持多种颜色主题 (gold/cyan/purple)
- 拆分 StockQuoteCard 子组件:GlassSection、LoadingSkeleton
- 更新 KeyMetrics、MainForceInfo、SecondaryQuote 使用 DEEP_SPACE_THEME
- 主组件从 540 行精简到 321 行(减少 40%)
- 删除重复的 GlowDecorations,统一使用 FUI/CardGlow

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 11:29:28 +08:00
parent 5b7534f6a5
commit 958222e75f
9 changed files with 649 additions and 413 deletions

View File

@@ -0,0 +1,140 @@
/**
* CardGlow - 卡片级装饰光效组件
*
* 为卡片提供 FUI 风格的装饰元素:
* - 顶部光条Ash Thorp 风格)
* - 角落发光效果James Turrell 风格)
* - 可选背景网格
*
* 与 AmbientGlow 的区别:
* - AmbientGlow: 页面级环境光position: fixed
* - CardGlow: 卡片级装饰光,相对于父容器定位
*
* @example
* ```tsx
* <Box position="relative" overflow="hidden">
* <CardGlow variant="gold" />
* {children}
* </Box>
* ```
*/
import React, { memo } from 'react';
import { Box } from '@chakra-ui/react';
export interface CardGlowProps {
/** 预设主题 */
variant?: 'gold' | 'cyan' | 'purple' | 'default';
/** 是否显示背景网格 */
showGrid?: boolean;
/** 自定义主色(覆盖 variant */
primaryColor?: string;
/** 自定义次色(覆盖 variant */
secondaryColor?: string;
}
// 预设颜色配置
const COLOR_PRESETS = {
gold: {
primary: 'rgba(212, 175, 55, 1)',
secondary: 'rgba(0, 212, 255, 0.1)',
grid: 'rgba(212, 175, 55, 0.03)',
},
cyan: {
primary: 'rgba(0, 212, 255, 1)',
secondary: 'rgba(212, 175, 55, 0.1)',
grid: 'rgba(0, 212, 255, 0.03)',
},
purple: {
primary: 'rgba(168, 85, 247, 1)',
secondary: 'rgba(0, 212, 255, 0.1)',
grid: 'rgba(168, 85, 247, 0.03)',
},
default: {
primary: 'rgba(255, 255, 255, 0.6)',
secondary: 'rgba(255, 255, 255, 0.1)',
grid: 'rgba(255, 255, 255, 0.02)',
},
};
/**
* 卡片装饰光效组件
*
* 纯展示组件,需要父容器设置 position: relative 和 overflow: hidden
*/
const CardGlow = memo<CardGlowProps>(({
variant = 'gold',
showGrid = true,
primaryColor,
secondaryColor,
}) => {
const preset = COLOR_PRESETS[variant];
const primary = primaryColor || preset.primary;
const secondary = secondaryColor || preset.secondary;
const gridColor = preset.grid;
return (
<>
{/* 顶部光条 - Ash Thorp 风格数据终端效果 */}
<Box
position="absolute"
top={0}
left="50%"
transform="translateX(-50%)"
width="60%"
height="1px"
background={`linear-gradient(90deg, transparent, ${primary}, transparent)`}
opacity={0.6}
pointerEvents="none"
/>
{/* 左上角光晕 - James Turrell 风格光影效果 */}
<Box
position="absolute"
top="-40px"
left="-40px"
width="80px"
height="80px"
borderRadius="50%"
background={`radial-gradient(circle, ${primary.replace('1)', '0.15)')} 0%, transparent 70%)`}
filter="blur(20px)"
pointerEvents="none"
/>
{/* 右下角光晕 - 补充色,增加层次感 */}
<Box
position="absolute"
bottom="-40px"
right="-40px"
width="80px"
height="80px"
borderRadius="50%"
background={`radial-gradient(circle, ${secondary} 0%, transparent 70%)`}
filter="blur(20px)"
pointerEvents="none"
/>
{/* 背景网格 - 微妙的科技感纹理 */}
{showGrid && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
backgroundImage={`
linear-gradient(${gridColor} 1px, transparent 1px),
linear-gradient(90deg, ${gridColor} 1px, transparent 1px)
`}
backgroundSize="40px 40px"
pointerEvents="none"
opacity={0.5}
/>
)}
</>
);
});
CardGlow.displayName = 'CardGlow';
export default CardGlow;

View File

@@ -1,12 +1,20 @@
/**
* FUI (Futuristic UI) 组件集合
* Ash Thorp 风格的科幻 UI 组件
*
* 组件说明:
* - FuiCorners: 科幻角落装饰
* - FuiContainer: FUI 风格容器
* - AmbientGlow: 页面级环境光效果position: fixed
* - CardGlow: 卡片级装饰光效(相对定位,用于卡片内部)
*/
export { default as FuiCorners } from './FuiCorners';
export { default as FuiContainer } from './FuiContainer';
export { default as AmbientGlow } from './AmbientGlow';
export { default as CardGlow } from './CardGlow';
export type { FuiCornersProps } from './FuiCorners';
export type { FuiContainerProps } from './FuiContainer';
export type { AmbientGlowProps } from './AmbientGlow';
export type { CardGlowProps } from './CardGlow';

View File

@@ -0,0 +1,79 @@
/**
* GlassSection - 玻璃态内嵌区块组件
*
* 用于包装数据区块(如估值指标、市值股本、主力动态)
* 提供统一的 FUI 风格容器样式:
* - 半透明背景
* - 金色边框高亮
* - 顶部光条装饰
* - 悬停效果
*/
import React, { memo } from 'react';
import { Box, Text } from '@chakra-ui/react';
import { DEEP_SPACE_THEME as T } from './theme';
export interface GlassSectionProps {
/** 区块标题 */
title: string;
/** 区块内容 */
children: React.ReactNode;
/** flex 布局属性,默认 1 */
flex?: number | string;
}
/**
* 玻璃态内嵌区块
*
* 提供统一的数据区块容器样式
* 用于包装 KeyMetrics、MainForceInfo 等内容组件
*/
export const GlassSection: React.FC<GlassSectionProps> = memo(({
title,
children,
flex = 1,
}) => (
<Box
flex={flex}
bg={T.bgInset}
borderRadius={T.radiusLG}
border={`1px solid ${T.borderGlass}`}
p={4}
position="relative"
transition={T.transitionFast}
_hover={{
borderColor: T.borderGoldHover,
bg: 'rgba(15, 18, 35, 0.6)',
}}
>
{/* 区块顶部金色光条装饰 */}
<Box
position="absolute"
top={0}
left="20px"
right="20px"
height="1px"
background={`linear-gradient(90deg, transparent, ${T.gold}40, transparent)`}
/>
{/* 区块标题 */}
<Text
fontSize="14px"
fontWeight="700"
color={T.gold}
mb={3}
textTransform="uppercase"
letterSpacing="0.1em"
textShadow={`0 0 12px ${T.gold}60`}
>
{title}
</Text>
{/* 区块内容 */}
{children}
</Box>
));
GlassSection.displayName = 'GlassSection';
export default GlassSection;

View File

@@ -1,23 +1,74 @@
/**
* KeyMetrics - 关键指标原子组件
* 显示 PE、EPS、PB、流通市值、52周波动
*
* 显示估值和市值相关指标:
* - 市盈率 (PE)
* - 流通市值
* - 发行总股本
* - 流通股本
* - 换手率
* - 52周波动
*
* 注意:标题由外层 GlassSection 提供
*/
import React, { memo } from 'react';
import { Box, VStack, HStack, Text } from '@chakra-ui/react';
import { VStack, HStack, Text } from '@chakra-ui/react';
import { formatPrice } from './formatters';
import { STOCK_CARD_THEME } from './theme';
import { DEEP_SPACE_THEME as T } from './theme';
export interface KeyMetricsProps {
/** 市盈率 */
pe: number;
/** 流通市值(已格式化字符串) */
marketCap: string;
totalShares?: number; // 发行总股本(亿股)
floatShares?: number; // 流通股本(亿股)
turnoverRate?: number; // 换手率(%
/** 发行总股本(亿股) */
totalShares?: number;
/** 流通股本(亿股) */
floatShares?: number;
/** 换手率(% */
turnoverRate?: number;
/** 52周最低价 */
week52Low: number;
/** 52周最高价 */
week52High: number;
}
/**
* 指标行组件 - 内部使用
*/
interface MetricRowProps {
label: string;
value: string | number;
valueColor?: string;
highlight?: boolean;
}
const MetricRow: React.FC<MetricRowProps> = ({
label,
value,
valueColor = T.textWhite,
highlight = false,
}) => (
<HStack justify="space-between" fontSize="13px">
<Text color={T.textMuted}>{label}</Text>
<Text
color={valueColor}
fontWeight={highlight ? '700' : '600'}
fontSize={highlight ? '15px' : '13px'}
textShadow={highlight ? `0 0 10px ${valueColor}40` : undefined}
>
{value}
</Text>
</HStack>
);
/**
* 关键指标展示组件
*
* 纯展示组件,不包含标题
* 应由 GlassSection 包装以提供标题
*/
export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
pe,
marketCap,
@@ -26,59 +77,38 @@ export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
turnoverRate,
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 ? pe.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}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{totalShares ? `${totalShares}亿股` : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{floatShares ? `${floatShares}亿股` : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{turnoverRate !== undefined ? `${turnoverRate.toFixed(2)}%` : '-'}
</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>
);
});
}) => (
<VStack align="stretch" spacing={2}>
<MetricRow
label="市盈率 (PE)"
value={pe ? pe.toFixed(2) : '-'}
valueColor={T.cyan}
highlight
/>
<MetricRow
label="流通市值"
value={marketCap || '-'}
valueColor={T.textPrimary}
highlight
/>
<MetricRow
label="发行总股本"
value={totalShares ? `${totalShares}亿股` : '-'}
/>
<MetricRow
label="流通股本"
value={floatShares ? `${floatShares}亿股` : '-'}
/>
<MetricRow
label="换手率"
value={turnoverRate !== undefined ? `${turnoverRate.toFixed(2)}%` : '-'}
valueColor={turnoverRate && turnoverRate > 5 ? T.orange : T.textWhite}
/>
<MetricRow
label="52周波动"
value={`${formatPrice(week52Low)} - ${formatPrice(week52High)}`}
/>
</VStack>
));
KeyMetrics.displayName = 'KeyMetrics';

View File

@@ -0,0 +1,117 @@
/**
* LoadingSkeleton - 股票行情卡片加载骨架屏
*
* 在数据加载期间展示的骨架屏组件
* 保持与实际内容相同的布局结构,提供良好的加载体验
*/
import React, { memo } from 'react';
import { Box, Flex, HStack, VStack, Skeleton } from '@chakra-ui/react';
import { CardGlow } from '@components/FUI';
import { glassCardStyle, DEEP_SPACE_THEME as T } from './theme';
/**
* 股票行情卡片加载骨架屏
*
* 布局结构对应实际卡片:
* - 头部:股票名称 + 操作按钮
* - 价格:当前价格 + 涨跌幅
* - 内容:多列数据区块
*/
export const LoadingSkeleton: React.FC = memo(() => (
<Box
{...glassCardStyle.containerGold}
p={8}
>
{/* 装饰性光效 */}
<CardGlow variant="gold" />
<VStack align="stretch" spacing={6} position="relative" zIndex={1}>
{/* 头部骨架:股票名称 + 代码 + 操作按钮 */}
<Flex justify="space-between">
<HStack spacing={3}>
<Skeleton
height="32px"
width="120px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusSM}
/>
<Skeleton
height="24px"
width="80px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusSM}
/>
</HStack>
<HStack spacing={2}>
<Skeleton
height="32px"
width="32px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusSM}
/>
<Skeleton
height="32px"
width="32px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusSM}
/>
</HStack>
</Flex>
{/* 价格骨架:当前价格 + 涨跌幅 Badge */}
<HStack>
<Skeleton
height="56px"
width="160px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusMD}
/>
<Skeleton
height="36px"
width="100px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusMD}
/>
</HStack>
{/* 内容骨架:三列数据区块 */}
<Flex gap={6}>
<Box flex={1}>
<Skeleton
height="120px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusLG}
/>
</Box>
<Box flex={1}>
<Skeleton
height="120px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusLG}
/>
</Box>
<Box flex={1}>
<Skeleton
height="120px"
startColor={T.bgInset}
endColor={T.borderGlass}
borderRadius={T.radiusLG}
/>
</Box>
</Flex>
</VStack>
</Box>
));
LoadingSkeleton.displayName = 'LoadingSkeleton';
export default LoadingSkeleton;

View File

@@ -1,70 +1,113 @@
/**
* MainForceInfo - 主力动态原子组件
* 显示主力净流入、机构持仓、买卖比例
*
* 显示主力资金和机构相关指标:
* - 主力净流入(带正负颜色)
* - 机构持仓比例
* - 买卖比例进度条
*
* 注意:标题由外层 GlassSection 提供
*/
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';
import { DEEP_SPACE_THEME as T } from './theme';
export interface MainForceInfoProps {
/** 主力净流入(亿) */
mainNetInflow: number;
/** 机构持仓比例(% */
institutionHolding: number;
/** 买入比例(% */
buyRatio: number;
/** 卖出比例(% */
sellRatio: number;
}
/**
* 指标行组件 - 内部使用
*/
interface MetricRowProps {
label: string;
value: string | number;
valueColor?: string;
highlight?: boolean;
}
const MetricRow: React.FC<MetricRowProps> = ({
label,
value,
valueColor = T.textWhite,
highlight = false,
}) => (
<HStack justify="space-between" fontSize="13px">
<Text color={T.textMuted}>{label}</Text>
<Text
color={valueColor}
fontWeight={highlight ? '700' : '600'}
fontSize={highlight ? '15px' : '13px'}
textShadow={highlight ? `0 0 10px ${valueColor}40` : undefined}
>
{value}
</Text>
</HStack>
);
/**
* 主力动态展示组件
*
* 纯展示组件,不包含标题
* 应由 GlassSection 包装以提供标题
*/
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;
const inflowColor = mainNetInflow >= 0 ? T.upColor : T.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)}
<VStack align="stretch" spacing={2}>
<MetricRow
label="主力净流入"
value={formatNetInflow(mainNetInflow)}
valueColor={inflowColor}
highlight
/>
<MetricRow
label="机构持仓"
value={`${institutionHolding.toFixed(2)}%`}
valueColor={T.purple}
highlight
/>
{/* 买卖比例进度条 */}
<Box mt={2}>
<Progress
value={buyRatio}
size="sm"
sx={{
'& > div': {
bg: T.upColor,
boxShadow: T.upGlow,
},
}}
bg={T.downColor}
borderRadius="full"
h="8px"
/>
<HStack justify="space-between" mt={2} fontSize="13px">
<Text color={T.upColor} fontWeight="600">
{buyRatio}%
</Text>
<Text color={T.downColor} fontWeight="600">
{sellRatio}%
</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>
</Box>
</VStack>
);
});

View File

@@ -1,59 +1,74 @@
/**
* SecondaryQuote - 次要行情原子组件
* 显示今开、昨收、最高、最低
*
* 显示今开、昨收、最高、最低等次要行情数据
* 使用水平布局,通过竖线分隔符分隔各项
*/
import React, { memo } from 'react';
import { HStack, Text } from '@chakra-ui/react';
import { Box, HStack, Text } from '@chakra-ui/react';
import { formatPrice } from './formatters';
import { STOCK_CARD_THEME } from './theme';
import { DEEP_SPACE_THEME as T } from './theme';
export interface SecondaryQuoteProps {
/** 今日开盘价 */
todayOpen: number;
/** 昨日收盘价 */
yesterdayClose: number;
/** 今日最高价 */
todayHigh: number;
/** 今日最低价 */
todayLow: number;
}
/**
* 竖线分隔符组件
*/
const Divider: React.FC = () => (
<Box w="1px" h="14px" bg={T.divider} />
);
/**
* 次要行情展示组件
*
* 水平排列展示今开、昨收、最高、最低
* 最高使用上涨颜色,最低使用下跌颜色
*/
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>
}) => (
<HStack spacing={6} fontSize="14px" flexWrap="wrap">
<Text color={T.textMuted}>
<Text as="span" color={T.textWhite} fontWeight="600" ml={1}>
{formatPrice(todayOpen)}
</Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={valueColor} fontWeight="bold">
{formatPrice(yesterdayClose)}
</Text>
</Text>
<Divider />
<Text color={T.textMuted}>
<Text as="span" color={T.textWhite} fontWeight="600" ml={1}>
{formatPrice(yesterdayClose)}
</Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={upColor} fontWeight="bold">
{formatPrice(todayHigh)}
</Text>
</Text>
<Divider />
<Text color={T.textMuted}>
<Text as="span" color={T.upColor} fontWeight="600" ml={1}>
{formatPrice(todayHigh)}
</Text>
<Text color={borderColor}>|</Text>
<Text color={labelColor}>
<Text as="span" color={downColor} fontWeight="bold">
{formatPrice(todayLow)}
</Text>
</Text>
<Divider />
<Text color={T.textMuted}>
<Text as="span" color={T.downColor} fontWeight="600" ml={1}>
{formatPrice(todayLow)}
</Text>
</HStack>
);
});
</Text>
</HStack>
));
SecondaryQuote.displayName = 'SecondaryQuote';

View File

@@ -1,8 +1,16 @@
/**
* StockQuoteCard 组件统一导出
*
* 组件分类:
* - 原子组件:基础展示组件(价格、指标、信息)
* - 容器组件:布局和装饰组件(玻璃容器、光效)
* - 复合组件:功能组件(对比、搜索)
* - 状态组件:加载、错误状态
*/
// 原子组件
// ============================================
// 原子组件 - 数据展示
// ============================================
export { PriceDisplay } from './PriceDisplay';
export { SecondaryQuote } from './SecondaryQuote';
export { KeyMetrics } from './KeyMetrics';
@@ -10,18 +18,36 @@ export { MainForceInfo } from './MainForceInfo';
export { CompanyInfo } from './CompanyInfo';
export { StockHeader } from './StockHeader';
// 复合组件
// ============================================
// 容器组件 - 布局
// ============================================
// 注意: 装饰光效组件已移至 @components/FUI/CardGlow
export { GlassSection } from './GlassSection';
// ============================================
// 状态组件 - 加载/错误
// ============================================
export { LoadingSkeleton } from './LoadingSkeleton';
// ============================================
// 复合组件 - 功能性
// ============================================
export { default as CompareStockInput } from './CompareStockInput';
export { default as StockCompareModal } from './StockCompareModal';
// ============================================
// 工具和主题
export { STOCK_CARD_THEME } from './theme';
// ============================================
export { STOCK_CARD_THEME, DEEP_SPACE_THEME, glassCardStyle } 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';
export type { GlassSectionProps } from './GlassSection';

View File

@@ -6,13 +6,19 @@
* - 光影深度,弥散背景光
* - 极致圆角,科幻数据终端感
*
* 保留原有所有功能:
* - 股票头部名称、代码、行业对比、关注、分享)
* - 价格显示(当前价、涨跌幅
* - 次要行情今开、昨收、最高、最低)
* - 关键指标PE、市值股本、换手率、52周
* - 主力动态(净流入、机构持仓、买卖比)
* - 公司信息(成立、注册资本、所在地、官网、简介)
* 功能模块
* - 股票头部名称、代码、行业标签、操作按钮(对比、关注、分享)
* - 价格展示:当前价、涨跌幅 Badge
* - 次要行情今开、昨收、最高、最低SecondaryQuote 组件
* - 数据区块:估值指标、市值股本、主力动态(三列 GlassSection 布局
*
* 组件结构:
* - GlowDecorations装饰性光效背景层
* - LoadingSkeleton加载骨架屏
* - GlassSection玻璃容器包装数据区块
* - SecondaryQuote次要行情展示
* - KeyMetrics关键指标估值 + 市值)
* - MainForceInfo主力动态
*/
import React, { memo } from 'react';
@@ -25,144 +31,38 @@ import {
Badge,
IconButton,
Tooltip,
Skeleton,
Progress,
Link,
Icon,
useDisclosure,
} from '@chakra-ui/react';
import { Share2, Calendar, Coins, MapPin, Globe } from 'lucide-react';
import { Share2 } from 'lucide-react';
import FavoriteButton from '@components/FavoriteButton';
import { CardGlow } from '@components/FUI';
import { StockCompareModal, CompareStockInput } from './components';
// 子组件导入
import {
StockCompareModal,
CompareStockInput,
LoadingSkeleton,
GlassSection,
SecondaryQuote,
MainForceInfo,
} from './components';
// Hooks 和工具
import { useStockQuoteData, useStockCompare } from './hooks';
import { DEEP_SPACE_THEME, glassCardStyle, decorativeElements } from './components/theme';
import { formatPrice, formatChangePercent, formatNetInflow } from './components/formatters';
import { formatRegisteredCapital, formatDate } from '../CompanyOverview/utils';
import { DEEP_SPACE_THEME, glassCardStyle } from './components/theme';
import { formatPrice, formatChangePercent } from './components/formatters';
import type { StockQuoteCardProps } from './types';
/** 主题常量简写 */
const T = DEEP_SPACE_THEME;
/**
* 装饰性光效组件
*/
const GlowDecorations: React.FC = () => (
<>
{/* 顶部金色光条 */}
<Box {...decorativeElements.topGlowBar} />
{/* 左上角光晕 */}
<Box
{...decorativeElements.cornerGlow}
top="-40px"
left="-40px"
/>
{/* 右下角光晕 */}
<Box
{...decorativeElements.cornerGlow}
bottom="-40px"
right="-40px"
background={`radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 70%)`}
/>
{/* 背景网格 */}
<Box {...decorativeElements.gridOverlay} />
</>
);
/**
* 加载骨架屏
*/
const LoadingSkeleton: React.FC = () => (
<Box
{...glassCardStyle.containerGold}
p={8}
>
<GlowDecorations />
<VStack align="stretch" spacing={6} position="relative" zIndex={1}>
{/* 头部骨架 */}
<Flex justify="space-between">
<HStack spacing={3}>
<Skeleton height="32px" width="120px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusSM} />
<Skeleton height="24px" width="80px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusSM} />
</HStack>
<HStack spacing={2}>
<Skeleton height="32px" width="32px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusSM} />
<Skeleton height="32px" width="32px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusSM} />
</HStack>
</Flex>
{/* 价格骨架 */}
<HStack>
<Skeleton height="56px" width="160px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusMD} />
<Skeleton height="36px" width="100px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusMD} />
</HStack>
{/* 内容骨架 */}
<Flex gap={6}>
<Box flex={1}>
<Skeleton height="120px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusLG} />
</Box>
<Box flex={1}>
<Skeleton height="120px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusLG} />
</Box>
</Flex>
</VStack>
</Box>
);
/**
* 玻璃态内嵌区块
*/
interface GlassSectionProps {
title: string;
children: React.ReactNode;
flex?: number | string;
}
const GlassSection: React.FC<GlassSectionProps> = ({ title, children, flex = 1 }) => (
<Box
flex={flex}
bg={T.bgInset}
borderRadius={T.radiusLG}
border={`1px solid ${T.borderGlass}`}
p={4}
position="relative"
transition={T.transitionFast}
_hover={{
borderColor: T.borderGoldHover,
bg: 'rgba(15, 18, 35, 0.6)',
}}
>
{/* 区块顶部光条 */}
<Box
position="absolute"
top={0}
left="20px"
right="20px"
height="1px"
background={`linear-gradient(90deg, transparent, ${T.gold}40, transparent)`}
/>
<Text
fontSize="14px"
fontWeight="700"
color={T.gold}
mb={3}
textTransform="uppercase"
letterSpacing="0.1em"
textShadow={`0 0 12px ${T.gold}60`}
>
{title}
</Text>
{children}
</Box>
);
/**
* 指标行组件
* 指标行组件 - 用于数据区块内的单行指标展示
*
* @param label - 指标标签
* @param value - 指标值
* @param valueColor - 值的颜色(默认白色)
* @param highlight - 是否高亮显示(加粗 + 发光)
*/
interface MetricRowProps {
label: string;
@@ -222,12 +122,11 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
return <LoadingSkeleton />;
}
// 涨跌判断
// 涨跌判断(用于价格颜色)
const isUp = quoteData.changePercent >= 0;
const priceColor = isUp ? T.upColor : T.downColor;
const priceGlow = isUp ? T.upGlow : T.downGlow;
const priceBg = isUp ? T.upColorMuted : T.downColorMuted;
const inflowColor = (quoteData.mainNetInflow || 0) >= 0 ? T.upColor : T.downColor;
return (
<>
@@ -235,7 +134,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
{...glassCardStyle.containerGold}
p={6}
>
<GlowDecorations />
<CardGlow variant="gold" />
{/* 内容区域(在装饰层之上)*/}
<VStack align="stretch" spacing={4} position="relative" zIndex={1}>
@@ -340,39 +239,16 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
</HStack>
{/* ========== 次要行情 ========== */}
<HStack spacing={6} fontSize="14px" flexWrap="wrap">
<Text color={T.textMuted}>
<Text as="span" color={T.textWhite} fontWeight="600" ml={1}>
{formatPrice(quoteData.todayOpen)}
</Text>
</Text>
<Box w="1px" h="14px" bg={T.divider} />
<Text color={T.textMuted}>
<Text as="span" color={T.textWhite} fontWeight="600" ml={1}>
{formatPrice(quoteData.yesterdayClose)}
</Text>
</Text>
<Box w="1px" h="14px" bg={T.divider} />
<Text color={T.textMuted}>
<Text as="span" color={T.upColor} fontWeight="600" ml={1}>
{formatPrice(quoteData.todayHigh)}
</Text>
</Text>
<Box w="1px" h="14px" bg={T.divider} />
<Text color={T.textMuted}>
<Text as="span" color={T.downColor} fontWeight="600" ml={1}>
{formatPrice(quoteData.todayLow)}
</Text>
</Text>
</HStack>
<SecondaryQuote
todayOpen={quoteData.todayOpen}
yesterdayClose={quoteData.yesterdayClose}
todayHigh={quoteData.todayHigh}
todayLow={quoteData.todayLow}
/>
{/* ========== 数据区块(三列布局)========== */}
<Flex gap={4} flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
{/* 第一列:估值指标 */}
{/* 第一列:估值指标 - PE、流通股本、换手率 */}
<GlassSection title="估值指标" flex={1}>
<VStack align="stretch" spacing={2}>
<MetricRow
@@ -393,7 +269,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
</VStack>
</GlassSection>
{/* 第二列:市值股本 */}
{/* 第二列:市值股本 - 流通市值、发行总股本、52周波动 */}
<GlassSection title="市值股本" flex={1}>
<VStack align="stretch" spacing={2}>
<MetricRow
@@ -415,114 +291,16 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
{/* 第三列:主力动态 */}
<GlassSection title="主力动态" flex={1}>
<VStack align="stretch" spacing={2}>
<MetricRow
label="主力净流入"
value={formatNetInflow(quoteData.mainNetInflow)}
valueColor={inflowColor}
highlight
/>
<MetricRow
label="机构持仓"
value={`${quoteData.institutionHolding.toFixed(2)}%`}
valueColor={T.purple}
highlight
/>
{/* 买卖比例条 */}
<Box mt={2}>
<Progress
value={quoteData.buyRatio}
size="sm"
sx={{
'& > div': {
bg: T.upColor,
boxShadow: T.upGlow,
},
}}
bg={T.downColor}
borderRadius="full"
h="8px"
/>
<HStack justify="space-between" mt={2} fontSize="13px">
<Text color={T.upColor} fontWeight="600">
{quoteData.buyRatio}%
</Text>
<Text color={T.downColor} fontWeight="600">
{quoteData.sellRatio}%
</Text>
</HStack>
</Box>
</VStack>
<MainForceInfo
mainNetInflow={quoteData.mainNetInflow || 0}
institutionHolding={quoteData.institutionHolding}
buyRatio={quoteData.buyRatio}
sellRatio={quoteData.sellRatio}
/>
</GlassSection>
</Flex>
{/* ========== 公司信息(已注释)========== */}
{/* {basicInfo && (
<>
<Box
h="1px"
bg={`linear-gradient(90deg, transparent, ${T.gold}30, transparent)`}
/>
<Flex gap={8} flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<HStack spacing={6} flex={1} flexWrap="wrap" fontSize="14px">
<HStack spacing={2}>
<Icon as={Calendar} color={T.textMuted} boxSize={4} />
<Text color={T.textMuted}>成立:</Text>
<Text color={T.textWhite} fontWeight="600">
{formatDate(basicInfo.establish_date)}
</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Coins} color={T.textMuted} boxSize={4} />
<Text color={T.textMuted}>注册资本:</Text>
<Text color={T.textWhite} fontWeight="600">
{formatRegisteredCapital(basicInfo.reg_capital)}
</Text>
</HStack>
<HStack spacing={2}>
<Icon as={MapPin} color={T.textMuted} boxSize={4} />
<Text color={T.textMuted}>所在地:</Text>
<Text color={T.textWhite} fontWeight="600">
{basicInfo.province} {basicInfo.city}
</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Globe} color={T.textMuted} boxSize={4} />
{basicInfo.website ? (
<Link
href={basicInfo.website}
isExternal
color={T.cyan}
fontWeight="600"
_hover={{ color: T.textPrimary, textDecoration: 'underline' }}
>
访问官网
</Link>
) : (
<Text color={T.textWhiteMuted}>暂无官网</Text>
)}
</HStack>
</HStack>
<Box
flex={2}
borderLeftWidth="1px"
borderColor={T.divider}
pl={8}
minW="0"
>
<Text fontSize="14px" color={T.textMuted} noOfLines={2}>
<Text as="span" fontWeight="700" color={T.textSecondary}>
公司简介:
</Text>
{basicInfo.company_intro || '暂无'}
</Text>
</Box>
</Flex>
</>
)} */}
{/* 公司信息区块已移至 CompanyOverview 模块 */}
</VStack>
</Box>