refactor(StockQuoteCard): 子组件提取与 DEEP_SPACE_THEME 统一

- PriceDisplay: 更新为 DEEP_SPACE_THEME,添加发光效果
- StockHeader: 更新为 DEEP_SPACE_THEME,金色边框和发光
- MetricRow: 新建指标行组件,支持普通和高亮模式
- 主组件从 321 行精简到 180 行(-44%)
- 统一使用提取的子组件,移除内联代码

🤖 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 13:03:21 +08:00
parent 82290e8a63
commit 61e159f29b
5 changed files with 164 additions and 205 deletions

View File

@@ -0,0 +1,48 @@
/**
* MetricRow - 指标行原子组件
*
* 用于数据区块内的单行指标展示
* 支持普通和高亮两种模式
*/
import React, { memo } from 'react';
import { HStack, Text } from '@chakra-ui/react';
import { DEEP_SPACE_THEME as T } from './theme';
export interface MetricRowProps {
/** 指标标签 */
label: string;
/** 指标值 */
value: string | number;
/** 值的颜色(默认白色) */
valueColor?: string;
/** 是否高亮显示(加粗 + 发光) */
highlight?: boolean;
}
/**
* 指标行组件
*
* 布局:左侧标签 + 右侧值
* 高亮模式:更大字号 + 发光效果
*/
export const MetricRow: React.FC<MetricRowProps> = memo(({
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>
));
MetricRow.displayName = 'MetricRow';

View File

@@ -1,38 +1,67 @@
/**
* PriceDisplay - 价格显示原子组件
* 显示当前价格和涨跌幅 Badge
*
* 深空 FUI 设计风格:
* - 大号价格数字,带涨跌色发光
* - Badge 使用半透明背景 + 边框发光
*/
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { HStack, Text, Badge } from '@chakra-ui/react';
import { formatPrice, formatChangePercent } from './formatters';
import { STOCK_CARD_THEME } from './theme';
import { DEEP_SPACE_THEME as T } from './theme';
export interface PriceDisplayProps {
/** 当前价格 */
currentPrice: number;
/** 涨跌幅(百分比) */
changePercent: number;
}
/**
* 价格显示组件
*
* 使用发光效果突出涨跌状态:
* - 涨:红色 (#FF4757) + 红色光晕
* - 跌:绿色 (#00D984) + 绿色光晕
*/
export const PriceDisplay: React.FC<PriceDisplayProps> = memo(({
currentPrice,
changePercent,
}) => {
const { upColor, downColor } = STOCK_CARD_THEME;
const priceColor = changePercent >= 0 ? upColor : downColor;
// 根据涨跌计算颜色和发光效果
const { priceColor, priceGlow, priceBg } = useMemo(() => {
const isUp = changePercent >= 0;
return {
priceColor: isUp ? T.upColor : T.downColor,
priceGlow: isUp ? T.upGlow : T.downGlow,
priceBg: isUp ? T.upColorMuted : T.downColorMuted,
};
}, [changePercent]);
return (
<HStack align="baseline" spacing={3} mb={3}>
<Text fontSize="48px" fontWeight="bold" color={priceColor}>
<HStack align="baseline" spacing={3}>
{/* 主价格 - 大号字体 + 发光 */}
<Text
fontSize="42px"
fontWeight="bold"
color={priceColor}
textShadow={priceGlow}
lineHeight="1"
>
{formatPrice(currentPrice)}
</Text>
{/* 涨跌幅 Badge - 半透明背景 + 边框发光 */}
<Badge
bg={changePercent >= 0 ? upColor : downColor}
color="#FFFFFF"
fontSize="20px"
bg={priceBg}
color={priceColor}
fontSize="18px"
fontWeight="bold"
px={3}
py={1}
borderRadius="md"
py={1.5}
borderRadius={T.radiusMD}
boxShadow={priceGlow}
>
{formatChangePercent(changePercent)}
</Badge>

View File

@@ -1,6 +1,10 @@
/**
* StockHeader - 股票头部原子组件
* 显示股票名称、代码、行业标签、指数标签、操作按钮
*
* 深空 FUI 设计风格:
* - 股票名称带金色发光
* - 行业标签使用金色边框
* - 操作按钮悬停态有玻璃效果
*/
import React, { memo } from 'react';
@@ -8,14 +12,20 @@ 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';
import { DEEP_SPACE_THEME as T } from './theme';
export interface StockHeaderProps {
/** 股票名称 */
name: string;
/** 股票代码 */
code: string;
/** 一级行业 */
industryL1?: string;
/** 二级行业 */
industry?: string;
/** 指数标签沪深300、中证500等 */
indexTags?: string[];
/** 更新时间 */
updateTime?: string;
// 关注相关
isInWatchlist?: boolean;
@@ -28,6 +38,13 @@ export interface StockHeaderProps {
onCompare?: (stockCode: string) => void;
}
/**
* 股票头部组件
*
* 包含:
* - 左侧:股票名称、代码、行业标签、指数标签
* - 右侧:对比输入、关注按钮、分享按钮、更新时间
*/
export const StockHeader: React.FC<StockHeaderProps> = memo(({
name,
code,
@@ -42,32 +59,35 @@ export const StockHeader: React.FC<StockHeaderProps> = memo(({
isCompareLoading = false,
onCompare,
}) => {
const { labelColor, valueColor, borderColor } = STOCK_CARD_THEME;
return (
<Flex justify="space-between" align="center" mb={4}>
<Flex justify="space-between" align="center">
{/* 左侧:股票名称 + 行业标签 + 指数标签 */}
<HStack spacing={3} align="center">
{/* 股票名称 - 突出显示 */}
<Text fontSize="26px" fontWeight="800" color={valueColor}>
<HStack spacing={4} align="center">
{/* 股票名称 - 金色发光效果 */}
<Text
fontSize="24px"
fontWeight="800"
color={T.textPrimary}
textShadow={`0 0 20px ${T.gold}40`}
>
{name}
</Text>
<Text fontSize="18px" fontWeight="normal" color={labelColor}>
<Text fontSize="16px" color={T.textMuted} fontWeight="normal">
({code})
</Text>
{/* 行业标签 */}
{/* 行业标签 - 金色边框 */}
{(industryL1 || industry) && (
<Badge
bg="transparent"
color={labelColor}
fontSize="14px"
fontWeight="medium"
color={T.textSecondary}
fontSize="13px"
fontWeight="500"
border="1px solid"
borderColor={borderColor}
px={2}
py={0.5}
borderRadius="md"
borderColor={T.borderGold}
px={3}
py={1}
borderRadius={T.radiusMD}
>
{industryL1 && industry
? `${industryL1} · ${industry}`
@@ -77,7 +97,7 @@ export const StockHeader: React.FC<StockHeaderProps> = memo(({
{/* 指数标签 */}
{indexTags && indexTags.length > 0 && (
<Text fontSize="14px" color={labelColor}>
<Text fontSize="13px" color={T.textMuted}>
{indexTags.join('、')}
</Text>
)}
@@ -103,13 +123,14 @@ export const StockHeader: React.FC<StockHeaderProps> = memo(({
aria-label="分享"
icon={<Share2 size={18} />}
variant="ghost"
color={labelColor}
color={T.textSecondary}
size="sm"
borderRadius={T.radiusSM}
onClick={onShare}
_hover={{ bg: 'whiteAlpha.100' }}
_hover={{ bg: T.borderGlass, color: T.textPrimary }}
/>
</Tooltip>
<Text fontSize="14px" color={labelColor}>
<Text fontSize="13px" color={T.textMuted}>
{updateTime?.split(' ')[1] || '--:--'}
</Text>
</HStack>

View File

@@ -17,6 +17,7 @@ export { KeyMetrics } from './KeyMetrics';
export { MainForceInfo } from './MainForceInfo';
export { CompanyInfo } from './CompanyInfo';
export { StockHeader } from './StockHeader';
export { MetricRow } from './MetricRow';
// ============================================
// 容器组件 - 布局
@@ -50,4 +51,5 @@ export type { KeyMetricsProps } from './KeyMetrics';
export type { MainForceInfoProps } from './MainForceInfo';
export type { CompanyInfoProps, CompanyBasicInfo } from './CompanyInfo';
export type { StockHeaderProps } from './StockHeader';
export type { MetricRowProps } from './MetricRow';
export type { GlassSectionProps } from './GlassSection';

View File

@@ -6,97 +6,44 @@
* - 光影深度,弥散背景光
* - 极致圆角,科幻数据终端感
*
* 功能模块:
* - 股票头部:名称、代码、行业标签、操作按钮(对比、关注、分享)
* - 价格展示:当前价格、涨跌幅 Badge
* - 次要行情今开、昨收、最高、最低SecondaryQuote 组件)
* - 数据区块:估值指标、市值股本、主力动态(三列 GlassSection 布局)
*
* 组件结构:
* - GlowDecorations装饰性光效背景层
* - LoadingSkeleton加载骨架屏
* - GlassSection玻璃容器包装数据区块
* - SecondaryQuote次要行情展示
* - KeyMetrics关键指标估值 + 市值)
* - StockHeader股票名称、代码、行业标签、操作按钮
* - PriceDisplay当前价格、涨跌幅 Badge
* - SecondaryQuote今开、昨收、最高、最低
* - GlassSection + MetricRow估值指标、市值股本
* - MainForceInfo主力动态
*/
import React, { memo } from 'react';
import {
Box,
Flex,
HStack,
VStack,
Text,
Badge,
IconButton,
Tooltip,
useDisclosure,
} from '@chakra-ui/react';
import { Share2 } from 'lucide-react';
import FavoriteButton from '@components/FavoriteButton';
import { Box, Flex, VStack, useDisclosure } from '@chakra-ui/react';
import { CardGlow } from '@components/FUI';
// 子组件导入
import {
StockCompareModal,
CompareStockInput,
LoadingSkeleton,
GlassSection,
StockHeader,
PriceDisplay,
SecondaryQuote,
MetricRow,
MainForceInfo,
DEEP_SPACE_THEME as T,
formatPrice,
} from './components';
import { glassCardStyle } from './components/theme';
// Hooks 和工具
// Hooks
import { useStockQuoteData, useStockCompare } from './hooks';
import { DEEP_SPACE_THEME, glassCardStyle } from './components/theme';
import { formatPrice, formatChangePercent } from './components/formatters';
import type { StockQuoteCardProps } from './types';
/** 主题常量简写 */
const T = DEEP_SPACE_THEME;
/**
* 指标行组件 - 用于数据区块内的单行指标展示
*
* @param label - 指标标签
* @param value - 指标值
* @param valueColor - 值的颜色(默认白色)
* @param highlight - 是否高亮显示(加粗 + 发光)
*/
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>
);
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
stockCode,
isInWatchlist = false,
isWatchlistLoading = false,
onWatchlistToggle,
}) => {
const { quoteData, basicInfo, isLoading } = useStockQuoteData(stockCode);
const { quoteData, isLoading } = useStockQuoteData(stockCode);
const {
currentStockInfo,
compareStockInfo,
@@ -122,121 +69,33 @@ 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;
return (
<>
<Box
{...glassCardStyle.containerGold}
p={6}
>
<Box {...glassCardStyle.containerGold} p={6}>
<CardGlow variant="gold" />
{/* 内容区域(在装饰层之上)*/}
<VStack align="stretch" spacing={4} position="relative" zIndex={1}>
{/* ========== 头部区域 ========== */}
<Flex justify="space-between" align="center">
{/* 左侧:股票名称 + 代码 + 行业 */}
<HStack spacing={4} align="center">
<Text
fontSize="24px"
fontWeight="800"
color={T.textPrimary}
textShadow={`0 0 20px ${T.gold}40`}
>
{quoteData.name}
</Text>
<Text fontSize="16px" color={T.textMuted} fontWeight="normal">
({quoteData.code})
</Text>
{/* 行业标签 */}
{(quoteData.industryL1 || quoteData.industry) && (
<Badge
bg="transparent"
color={T.textSecondary}
fontSize="13px"
fontWeight="500"
border="1px solid"
borderColor={T.borderGold}
px={3}
py={1}
borderRadius={T.radiusMD}
>
{quoteData.industryL1 && quoteData.industry
? `${quoteData.industryL1} · ${quoteData.industry}`
: quoteData.industry || quoteData.industryL1}
</Badge>
)}
{/* 指数标签 */}
{quoteData.indexTags && quoteData.indexTags.length > 0 && (
<Text fontSize="13px" color={T.textMuted}>
{quoteData.indexTags.join('、')}
</Text>
)}
</HStack>
{/* 右侧:操作按钮 */}
<HStack spacing={3}>
<CompareStockInput
<StockHeader
name={quoteData.name}
code={quoteData.code}
industryL1={quoteData.industryL1}
industry={quoteData.industry}
indexTags={quoteData.indexTags}
updateTime={quoteData.updateTime}
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={onWatchlistToggle}
isCompareLoading={isCompareLoading}
onCompare={handleCompare}
isLoading={isCompareLoading}
currentStockCode={quoteData.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={T.textSecondary}
size="sm"
borderRadius={T.radiusSM}
_hover={{ bg: T.borderGlass, color: T.textPrimary }}
/>
</Tooltip>
<Text fontSize="13px" color={T.textMuted}>
{quoteData.updateTime?.split(' ')[1] || '--:--'}
</Text>
</HStack>
</Flex>
{/* ========== 价格区域 ========== */}
<HStack align="baseline" spacing={3}>
<Text
fontSize="42px"
fontWeight="bold"
color={priceColor}
textShadow={priceGlow}
lineHeight="1"
>
{formatPrice(quoteData.currentPrice)}
</Text>
<Badge
bg={priceBg}
color={priceColor}
fontSize="18px"
fontWeight="bold"
px={3}
py={1.5}
borderRadius={T.radiusMD}
boxShadow={priceGlow}
>
{formatChangePercent(quoteData.changePercent)}
</Badge>
</HStack>
<PriceDisplay
currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
/>
{/* ========== 次要行情 ========== */}
<SecondaryQuote