更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-17 22:40:27 +08:00
parent 0214052965
commit 983d2575b2
3 changed files with 761 additions and 378 deletions

View File

@@ -1,8 +1,10 @@
/**
* SubTabContainer - 二级导航容器组件
*
* 用于模块内的子功能切换(如公司档案下的股权结构、管理团队等
* 与 TabContainer一级导航区分无 Card 包裹,直接融入父容器
* 深空 FUI 设计风格Glassmorphism + Ash Thorp + James Turrell
* - 玻璃态导航栏,漂浮感
* - 选中态发光效果,科幻数据终端感
* - 流畅的过渡动画
*
* @example
* ```tsx
@@ -43,6 +45,40 @@ export interface SubTabConfig {
component?: ComponentType<any>;
}
/**
* 深空 FUI 主题配置
*/
const DEEP_SPACE = {
// 背景
bgGlass: 'rgba(12, 14, 28, 0.6)',
bgGlassHover: 'rgba(18, 22, 42, 0.7)',
// 边框
borderGold: 'rgba(212, 175, 55, 0.2)',
borderGoldHover: 'rgba(212, 175, 55, 0.5)',
borderGlass: 'rgba(255, 255, 255, 0.06)',
// 发光
glowGold: '0 0 30px rgba(212, 175, 55, 0.25), 0 4px 20px rgba(0, 0, 0, 0.3)',
innerGlow: 'inset 0 1px 0 rgba(255, 255, 255, 0.08)',
// 文字
textWhite: 'rgba(255, 255, 255, 0.95)',
textMuted: 'rgba(255, 255, 255, 0.6)',
textGold: '#F4D03F',
textDark: '#0A0A14',
// 选中态
selectedBg: 'linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(184, 150, 12, 0.95) 100%)',
// 圆角
radius: '12px',
radiusLG: '16px',
// 动画
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
};
/**
* 主题配置
*/
@@ -56,16 +92,16 @@ export interface SubTabTheme {
}
/**
* 预设主题 - FUI 风格优化
* 预设主题 - 深空 FUI 风格
*/
const THEME_PRESETS: Record<string, SubTabTheme> = {
blackGold: {
bg: 'transparent',
borderColor: 'rgba(212, 175, 55, 0.15)',
tabSelectedBg: 'linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(184, 150, 12, 0.95) 100%)',
tabSelectedColor: '#0A0A14',
tabUnselectedColor: 'rgba(255, 255, 255, 0.85)', // 调亮:白色更清晰
tabHoverBg: 'rgba(212, 175, 55, 0.15)',
bg: DEEP_SPACE.bgGlass,
borderColor: DEEP_SPACE.borderGold,
tabSelectedBg: DEEP_SPACE.selectedBg,
tabSelectedColor: DEEP_SPACE.textDark,
tabUnselectedColor: DEEP_SPACE.textWhite,
tabHoverBg: DEEP_SPACE.bgGlassHover,
},
default: {
bg: 'white',
@@ -158,74 +194,102 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
index={currentIndex}
onChange={handleTabChange}
>
{/* TabList - 玻璃态导航栏 */}
<TabList
bg={theme.bg}
backdropFilter="blur(20px)"
WebkitBackdropFilter="blur(20px)"
borderBottom="1px solid"
borderColor={theme.borderColor}
pl={2}
pr={4}
borderRadius={DEEP_SPACE.radiusLG}
mx={2}
mb={2}
px={3}
py={3}
flexWrap="nowrap"
gap={2}
alignItems="center"
overflowX="auto"
position="relative"
boxShadow={DEEP_SPACE.innerGlow}
css={{
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
}}
>
{tabs.map((tab) => (
<Tab
key={tab.key}
color={theme.tabUnselectedColor}
borderRadius="md"
px={5}
py={2.5}
fontSize="sm"
fontWeight="500"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
position="relative"
letterSpacing="0.05em"
transition="all 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
_before={{
content: '""',
position: 'absolute',
bottom: '-1px',
left: '50%',
transform: 'translateX(-50%)',
width: '0%',
height: '2px',
bg: '#D4AF37',
transition: 'width 0.25s ease',
}}
_selected={{
bg: theme.tabSelectedBg,
color: theme.tabSelectedColor,
fontWeight: '700',
boxShadow: '0 4px 16px rgba(212, 175, 55, 0.35), 0 0 20px rgba(212, 175, 55, 0.15)',
border: '1px solid rgba(212, 175, 55, 0.6)',
transform: 'translateY(-1px)',
_before: {
width: '80%',
},
}}
_hover={{
bg: theme.tabHoverBg,
border: '1px solid rgba(212, 175, 55, 0.35)',
transform: 'translateY(-1px)',
_before: {
width: '60%',
},
}}
>
<HStack spacing={2}>
{tab.icon && <Icon as={tab.icon} boxSize={4} />}
<Text>{tab.name}</Text>
</HStack>
</Tab>
))}
{/* 顶部金色光条 */}
<Box
position="absolute"
top={0}
left="50%"
transform="translateX(-50%)"
width="50%"
height="1px"
background={`linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent)`}
/>
{tabs.map((tab, idx) => {
const isSelected = idx === currentIndex;
return (
<Tab
key={tab.key}
color={theme.tabUnselectedColor}
borderRadius={DEEP_SPACE.radius}
px={6}
py={3}
fontSize="15px"
fontWeight="500"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
position="relative"
letterSpacing="0.03em"
transition={DEEP_SPACE.transition}
_before={{
content: '""',
position: 'absolute',
bottom: '-1px',
left: '50%',
transform: 'translateX(-50%)',
width: isSelected ? '70%' : '0%',
height: '2px',
bg: '#D4AF37',
borderRadius: 'full',
transition: 'width 0.3s ease',
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
}}
_selected={{
bg: theme.tabSelectedBg,
color: theme.tabSelectedColor,
fontWeight: '700',
boxShadow: DEEP_SPACE.glowGold,
border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
transform: 'translateY(-2px)',
}}
_hover={{
bg: isSelected ? undefined : theme.tabHoverBg,
border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`,
transform: 'translateY(-1px)',
}}
_active={{
transform: 'translateY(0)',
}}
>
<HStack spacing={2}>
{tab.icon && (
<Icon
as={tab.icon}
boxSize={4}
opacity={isSelected ? 1 : 0.7}
transition="opacity 0.2s"
/>
)}
<Text>{tab.name}</Text>
</HStack>
</Tab>
);
})}
{rightElement && (
<>
<Spacer />

View File

@@ -1,20 +1,193 @@
/**
* StockQuoteCard 黑金主题配置
* StockQuoteCard 深空 FUI 主题配置
*
* 设计灵感:
* - Ash Thorp / Linear.app 科幻 FUI 风格
* - James Turrell 光影艺术
* - Glassmorphism 玻璃态设计
* - 深空漂浮的数据终端
*/
export const STOCK_CARD_THEME = {
// 背景和边框
cardBg: '#1A202C',
borderColor: '#C9A961',
// ============================================
// 深空 FUI 主题系统
// ============================================
export const DEEP_SPACE_THEME = {
// === 深空背景层 ===
// 最深层背景(宇宙深邃)
bgDeep: 'rgba(8, 8, 18, 0.98)',
// 玻璃卡片背景(半透明漂浮)
bgGlass: 'rgba(15, 18, 35, 0.65)',
bgGlassHover: 'rgba(20, 25, 50, 0.75)',
// 内嵌区块背景
bgInset: 'rgba(10, 12, 25, 0.5)',
// 文字颜色
labelColor: '#C9A961',
valueColor: '#F4D03F',
sectionTitleColor: '#F4D03F',
// === Glassmorphism 效果 ===
blur: '24px',
blurLight: '12px',
// 涨跌颜色(红涨绿跌)
upColor: '#F44336',
downColor: '#4CAF50',
// === 边框系统 ===
// 主边框(发光金边)
borderGold: 'rgba(212, 175, 55, 0.4)',
borderGoldHover: 'rgba(212, 175, 55, 0.7)',
// 玻璃边框(微妙白边)
borderGlass: 'rgba(255, 255, 255, 0.08)',
borderGlassHover: 'rgba(255, 255, 255, 0.15)',
// 分隔线
divider: 'rgba(212, 175, 55, 0.15)',
// === 发光系统James Turrell 风格)===
// 金色光晕
glowGold: '0 0 40px rgba(212, 175, 55, 0.15), 0 0 80px rgba(212, 175, 55, 0.08)',
glowGoldHover: '0 0 60px rgba(212, 175, 55, 0.25), 0 0 120px rgba(212, 175, 55, 0.12)',
// 青色光晕(科技感)
glowCyan: '0 0 30px rgba(0, 212, 255, 0.2), 0 0 60px rgba(0, 212, 255, 0.1)',
// 漂浮阴影(深度感)
floatShadow: '0 20px 60px rgba(0, 0, 0, 0.4), 0 8px 20px rgba(0, 0, 0, 0.3)',
// 内发光Turrell 光隧道效果)
innerGlow: 'inset 0 1px 0 rgba(255, 255, 255, 0.05), inset 0 -1px 0 rgba(0, 0, 0, 0.2)',
// === 圆角系统(极致圆角)===
radiusXL: '24px',
radiusLG: '16px',
radiusMD: '12px',
radiusSM: '8px',
// === 文字颜色 ===
// 主文字(高亮金色)
textPrimary: '#F4D03F',
// 次要文字(柔和金色)
textSecondary: 'rgba(212, 175, 55, 0.85)',
// 标签文字(低对比度)
textMuted: 'rgba(180, 160, 120, 0.7)',
// 纯白文字
textWhite: 'rgba(255, 255, 255, 0.95)',
textWhiteMuted: 'rgba(255, 255, 255, 0.6)',
// === 涨跌颜色(红涨绿跌)===
upColor: '#FF4757',
upColorMuted: 'rgba(255, 71, 87, 0.15)',
upGlow: '0 0 20px rgba(255, 71, 87, 0.3)',
downColor: '#00D984',
downColorMuted: 'rgba(0, 217, 132, 0.15)',
downGlow: '0 0 20px rgba(0, 217, 132, 0.3)',
// === 强调色 ===
gold: '#D4AF37',
goldBright: '#F4D03F',
cyan: '#00D4FF',
purple: '#A855F7',
orange: '#FF6B35',
// === 动画 ===
transitionFast: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
transitionNormal: 'all 0.35s cubic-bezier(0.4, 0, 0.2, 1)',
transitionSlow: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
} as const;
// 兼容旧组件的导出
export const STOCK_CARD_THEME = {
cardBg: DEEP_SPACE_THEME.bgGlass,
borderColor: DEEP_SPACE_THEME.borderGold,
labelColor: DEEP_SPACE_THEME.textSecondary,
valueColor: DEEP_SPACE_THEME.textPrimary,
sectionTitleColor: DEEP_SPACE_THEME.textPrimary,
upColor: DEEP_SPACE_THEME.upColor,
downColor: DEEP_SPACE_THEME.downColor,
} as const;
// ============================================
// 玻璃卡片样式生成器
// ============================================
export const glassCardStyle = {
// 主容器玻璃效果
container: {
bg: DEEP_SPACE_THEME.bgGlass,
backdropFilter: `blur(${DEEP_SPACE_THEME.blur})`,
WebkitBackdropFilter: `blur(${DEEP_SPACE_THEME.blur})`,
borderRadius: DEEP_SPACE_THEME.radiusXL,
border: `1px solid ${DEEP_SPACE_THEME.borderGlass}`,
boxShadow: `${DEEP_SPACE_THEME.floatShadow}, ${DEEP_SPACE_THEME.innerGlow}`,
position: 'relative' as const,
overflow: 'hidden' as const,
transition: DEEP_SPACE_THEME.transitionNormal,
_hover: {
bg: DEEP_SPACE_THEME.bgGlassHover,
borderColor: DEEP_SPACE_THEME.borderGoldHover,
boxShadow: `${DEEP_SPACE_THEME.glowGoldHover}, ${DEEP_SPACE_THEME.floatShadow}`,
transform: 'translateY(-2px)',
},
},
// 金色高亮边框版本
containerGold: {
bg: DEEP_SPACE_THEME.bgGlass,
backdropFilter: `blur(${DEEP_SPACE_THEME.blur})`,
WebkitBackdropFilter: `blur(${DEEP_SPACE_THEME.blur})`,
borderRadius: DEEP_SPACE_THEME.radiusXL,
border: `1px solid ${DEEP_SPACE_THEME.borderGold}`,
boxShadow: `${DEEP_SPACE_THEME.glowGold}, ${DEEP_SPACE_THEME.floatShadow}, ${DEEP_SPACE_THEME.innerGlow}`,
position: 'relative' as const,
overflow: 'hidden' as const,
transition: DEEP_SPACE_THEME.transitionNormal,
_hover: {
bg: DEEP_SPACE_THEME.bgGlassHover,
borderColor: DEEP_SPACE_THEME.borderGoldHover,
boxShadow: `${DEEP_SPACE_THEME.glowGoldHover}, ${DEEP_SPACE_THEME.floatShadow}`,
transform: 'translateY(-2px)',
},
},
// 内嵌区块
insetSection: {
bg: DEEP_SPACE_THEME.bgInset,
borderRadius: DEEP_SPACE_THEME.radiusLG,
border: `1px solid ${DEEP_SPACE_THEME.borderGlass}`,
p: 4,
},
};
// ============================================
// 装饰元素
// ============================================
export const decorativeElements = {
// 顶部金色光条Ash Thorp 风格)
topGlowBar: {
position: 'absolute' as const,
top: 0,
left: '50%',
transform: 'translateX(-50%)',
width: '60%',
height: '1px',
background: `linear-gradient(90deg, transparent, ${DEEP_SPACE_THEME.gold}, transparent)`,
opacity: 0.6,
},
// 角落光点
cornerGlow: {
position: 'absolute' as const,
width: '80px',
height: '80px',
borderRadius: '50%',
background: `radial-gradient(circle, rgba(212, 175, 55, 0.15) 0%, transparent 70%)`,
filter: 'blur(20px)',
},
// 背景网格(微妙的科技感)
gridOverlay: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: `
linear-gradient(rgba(212, 175, 55, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(212, 175, 55, 0.03) 1px, transparent 1px)
`,
backgroundSize: '40px 40px',
pointerEvents: 'none' as const,
opacity: 0.5,
},
};
export type DeepSpaceTheme = typeof DEEP_SPACE_THEME;
export type StockCardTheme = typeof STOCK_CARD_THEME;

View File

@@ -1,246 +1,193 @@
/**
* StockQuoteCard - 股票行情卡片组件
*
* 采用四卡片布局 FUI 风格设计
* 参考:交易热度、估值安全、情绪风险等分区展示
* 深空 FUI 设计风格Glassmorphism + Ash Thorp + James Turrell
* - 半透明玻璃态卡片,漂浮在深空中
* - 光影深度,弥散背景光
* - 极致圆角,科幻数据终端感
*
* 保留原有所有功能:
* - 股票头部(名称、代码、行业、对比、关注、分享)
* - 价格显示(当前价、涨跌幅)
* - 次要行情(今开、昨收、最高、最低)
* - 关键指标PE、市值、股本、换手率、52周
* - 主力动态(净流入、机构持仓、买卖比)
* - 公司信息(成立、注册资本、所在地、官网、简介)
*/
import React from 'react';
import {
Box,
Flex,
Grid,
VStack,
HStack,
Skeleton,
VStack,
Text,
Badge,
IconButton,
Tooltip,
Skeleton,
Progress,
Link,
Icon,
useDisclosure,
} from '@chakra-ui/react';
import { TrendingUp, Activity, DollarSign, AlertTriangle } from 'lucide-react';
import { Share2, Calendar, Coins, MapPin, Globe } from 'lucide-react';
import FavoriteButton from '@components/FavoriteButton';
import { StockCompareModal } from './components';
import { StockCompareModal, CompareStockInput } from './components';
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 type { StockQuoteCardProps } from './types';
// FUI 主题色彩
const FUI = {
gold: '#D4AF37',
orange: '#FF6B35',
green: '#00D984',
red: '#FF4757',
cyan: '#00D4FF',
purple: '#A855F7',
bgCard: 'rgba(20, 20, 30, 0.95)',
bgCardHover: 'rgba(30, 30, 45, 0.98)',
border: 'rgba(255, 255, 255, 0.08)',
borderGold: 'rgba(212, 175, 55, 0.3)',
text: 'rgba(255, 255, 255, 0.95)',
textMuted: 'rgba(255, 255, 255, 0.6)',
};
const T = DEEP_SPACE_THEME;
// 单个指标卡片组件
interface MetricCardProps {
icon: React.ElementType;
iconColor: string;
title: string;
badge?: string;
badgeColor?: string;
mainValue: string | number;
mainColor?: string;
mainUnit?: string;
subItems: Array<{ label: string; value: string | number; color?: string }>;
rightIcon?: React.ReactNode;
}
/**
* 装饰性光效组件
*/
const GlowDecorations: React.FC = () => (
<>
{/* 顶部金色光条 */}
<Box {...decorativeElements.topGlowBar} />
const MetricCard: React.FC<MetricCardProps> = ({
icon: IconComponent,
iconColor,
title,
badge,
badgeColor = FUI.textMuted,
mainValue,
mainColor = FUI.orange,
mainUnit,
subItems,
rightIcon,
}) => (
<Box
bg={FUI.bgCard}
borderRadius="lg"
border="1px solid"
borderColor={FUI.border}
p={4}
position="relative"
overflow="hidden"
transition="all 0.25s ease"
_hover={{
bg: FUI.bgCardHover,
borderColor: FUI.borderGold,
transform: 'translateY(-2px)',
boxShadow: `0 8px 24px rgba(0, 0, 0, 0.3), 0 0 1px ${FUI.gold}`,
}}
>
{/* 左上角光条 */}
{/* 左上角光晕 */}
<Box
position="absolute"
top={0}
left={0}
w="40px"
h="2px"
bg={iconColor}
opacity={0.8}
{...decorativeElements.cornerGlow}
top="-40px"
left="-40px"
/>
{/* 顶部:图标 + 标题 + 标签 */}
<HStack spacing={2} mb={3}>
<Icon as={IconComponent} boxSize={4} color={iconColor} />
<Text fontSize="sm" fontWeight="600" color={FUI.text}>
{title}
</Text>
{badge && (
<Badge
fontSize="10px"
px={2}
py={0.5}
bg="rgba(255, 255, 255, 0.1)"
color={badgeColor}
borderRadius="sm"
fontWeight="500"
>
{badge}
</Badge>
)}
{rightIcon && (
<Box ml="auto" opacity={0.5}>
{rightIcon}
</Box>
)}
</HStack>
{/* 右下角光晕 */}
<Box
{...decorativeElements.cornerGlow}
bottom="-40px"
right="-40px"
background={`radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 70%)`}
/>
{/* 主数值 */}
<Text
fontSize="2xl"
fontWeight="bold"
color={mainColor}
lineHeight="1.2"
mb={2}
>
{mainValue}
{mainUnit && (
<Text as="span" fontSize="md" fontWeight="normal" ml={1}>
{mainUnit}
</Text>
)}
</Text>
{/* 背景网格 */}
<Box {...decorativeElements.gridOverlay} />
</>
);
{/* 子项目 */}
<VStack align="stretch" spacing={1}>
{subItems.map((item, idx) => (
<HStack key={idx} justify="space-between" fontSize="xs">
<Text color={FUI.textMuted}>{item.label}</Text>
<Text color={item.color || FUI.text} fontWeight="500">
{item.value}
</Text>
/**
* 加载骨架屏
*/
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 StockInfoCardProps {
name: string;
code: string;
currentPrice: number;
changePercent: number;
industry?: string;
/**
* 玻璃态内嵌区块
*/
interface GlassSectionProps {
title: string;
children: React.ReactNode;
flex?: number | string;
}
const StockInfoCard: React.FC<StockInfoCardProps> = ({
name,
code,
currentPrice,
changePercent,
industry,
}) => {
const isUp = changePercent >= 0;
const priceColor = isUp ? FUI.red : FUI.green;
const trendText = isUp ? '强势上涨' : '震荡下跌';
return (
const GlassSection: React.FC<GlassSectionProps> = ({ title, children, flex = 1 }) => (
<Box
flex={flex}
bg={T.bgInset}
borderRadius={T.radiusLG}
border={`1px solid ${T.borderGlass}`}
p={5}
position="relative"
transition={T.transitionFast}
_hover={{
borderColor: T.borderGoldHover,
bg: 'rgba(15, 18, 35, 0.6)',
}}
>
{/* 区块顶部光条 */}
<Box
bg={FUI.bgCard}
borderRadius="lg"
border="1px solid"
borderColor={FUI.border}
p={4}
position="relative"
overflow="hidden"
transition="all 0.25s ease"
_hover={{
bg: FUI.bgCardHover,
borderColor: FUI.borderGold,
transform: 'translateY(-2px)',
boxShadow: `0 8px 24px rgba(0, 0, 0, 0.3), 0 0 1px ${FUI.gold}`,
}}
position="absolute"
top={0}
left="20px"
right="20px"
height="1px"
background={`linear-gradient(90deg, transparent, ${T.gold}40, transparent)`}
/>
<Text
fontSize="13px"
fontWeight="600"
color={T.textSecondary}
mb={3}
textTransform="uppercase"
letterSpacing="0.1em"
>
{/* 左上角光条 */}
<Box
position="absolute"
top={0}
left={0}
w="40px"
h="2px"
bg={priceColor}
opacity={0.8}
/>
{title}
</Text>
{children}
</Box>
);
{/* 股票名称和代码 */}
<HStack spacing={2} mb={3}>
<Text fontSize="lg" fontWeight="bold" color={FUI.text}>
{name}
</Text>
<Badge
fontSize="10px"
px={2}
py={0.5}
bg="rgba(255, 255, 255, 0.1)"
color={FUI.textMuted}
borderRadius="sm"
>
{code}
</Badge>
</HStack>
/**
* 指标行组件
*/
interface MetricRowProps {
label: string;
value: string | number;
valueColor?: string;
highlight?: boolean;
}
{/* 价格和涨跌幅 */}
<HStack spacing={3} mb={2}>
<Text fontSize="2xl" fontWeight="bold" color={priceColor}>
{currentPrice.toFixed(2)}
</Text>
<Badge
fontSize="sm"
px={2}
py={1}
bg={isUp ? 'rgba(255, 71, 87, 0.15)' : 'rgba(0, 217, 132, 0.15)'}
color={priceColor}
borderRadius="md"
fontWeight="bold"
>
{isUp ? '↗' : '↘'} {isUp ? '+' : ''}{changePercent.toFixed(2)}%
</Badge>
</HStack>
{/* 走势标签 */}
<HStack fontSize="xs" color={FUI.textMuted}>
<Icon as={TrendingUp} boxSize={3} color={priceColor} />
<Text></Text>
<Text color={priceColor} fontWeight="500">{trendText}</Text>
</HStack>
</Box>
);
};
const MetricRow: React.FC<MetricRowProps> = ({
label,
value,
valueColor = T.textWhite,
highlight = false,
}) => (
<HStack justify="space-between" fontSize="14px">
<Text color={T.textMuted}>{label}</Text>
<Text
color={valueColor}
fontWeight={highlight ? '700' : '600'}
fontSize={highlight ? '16px' : '14px'}
textShadow={highlight ? `0 0 10px ${valueColor}40` : undefined}
>
{value}
</Text>
</HStack>
);
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
stockCode,
@@ -269,112 +216,311 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
clearCompare();
};
// 加载中骨架屏
// 加载中
if (isLoading || !quoteData) {
return (
<Grid templateColumns="repeat(4, 1fr)" gap={4}>
{[1, 2, 3, 4].map((i) => (
<Box
key={i}
bg={FUI.bgCard}
borderRadius="lg"
border="1px solid"
borderColor={FUI.border}
p={4}
>
<Skeleton height="20px" width="120px" mb={3} startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
<Skeleton height="32px" width="80px" mb={2} startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
<Skeleton height="14px" width="100%" startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
</Box>
))}
</Grid>
);
return <LoadingSkeleton />;
}
// 格式化数值
const formatVolume = (val: number) => {
if (!val) return '-';
if (val >= 100000000) return `${(val / 100000000).toFixed(2)}亿`;
if (val >= 10000) return `${(val / 10000).toFixed(0)}`;
return val.toString();
};
// 涨跌判断
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 (
<>
<Grid templateColumns="repeat(4, 1fr)" gap={4}>
{/* 卡片1: 股票基本信息 */}
<StockInfoCard
name={quoteData.name}
code={quoteData.code}
currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
industry={quoteData.industry}
/>
<Box
{...glassCardStyle.containerGold}
p={8}
>
<GlowDecorations />
{/* 卡片2: 交易热度 */}
<MetricCard
icon={Activity}
iconColor={FUI.orange}
title="交易热度"
badge="流动性"
mainValue={quoteData.marketCap || '-'}
mainColor={FUI.orange}
subItems={[
{
label: '换手率',
value: quoteData.turnoverRate ? `${quoteData.turnoverRate.toFixed(2)}%` : '-',
color: (quoteData.turnoverRate || 0) > 5 ? FUI.orange : FUI.text,
},
{
label: '52周区间',
value: `${quoteData.week52Low?.toFixed(2) || '-'} - ${quoteData.week52High?.toFixed(2) || '-'}`,
},
]}
/>
{/* 内容区域(在装饰层之上)*/}
<VStack align="stretch" spacing={6} position="relative" zIndex={1}>
{/* 卡片3: 估值安全 */}
<MetricCard
icon={DollarSign}
iconColor={FUI.cyan}
title="估值 VS 安全"
badge="便宜否"
mainValue={quoteData.pe ? quoteData.pe.toFixed(2) : '-'}
mainColor={FUI.cyan}
mainUnit=""
subItems={[
{
label: '市盈率(PE)',
value: quoteData.pe ? (quoteData.pe > 50 ? '高估值' : quoteData.pe > 20 ? '合理' : '低估值') : '-',
color: quoteData.pe ? (quoteData.pe > 50 ? FUI.orange : quoteData.pe > 20 ? FUI.gold : FUI.green) : FUI.textMuted,
},
{
label: '发行总股本',
value: quoteData.totalShares ? `${quoteData.totalShares}亿股` : '-',
},
]}
/>
{/* ========== 头部区域 ========== */}
<Flex justify="space-between" align="center">
{/* 左侧:股票名称 + 代码 + 行业 */}
<HStack spacing={4} align="center">
<Text
fontSize="28px"
fontWeight="800"
color={T.textPrimary}
textShadow={`0 0 20px ${T.gold}40`}
>
{quoteData.name}
</Text>
<Text fontSize="18px" color={T.textMuted} fontWeight="normal">
({quoteData.code})
</Text>
{/* 卡片4: 股本结构 */}
<MetricCard
icon={AlertTriangle}
iconColor={FUI.green}
title="股本结构"
badge="资金面"
mainValue={quoteData.floatShares ? `${quoteData.floatShares}` : '-'}
mainColor={FUI.green}
mainUnit="亿股"
subItems={[
{
label: '流通股本',
value: quoteData.floatShares ? `${quoteData.floatShares}亿股` : '-',
},
{
label: '行业',
value: quoteData.industry || '-',
},
]}
/>
</Grid>
{/* 行业标签 */}
{(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
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={4}>
<Text
fontSize="52px"
fontWeight="bold"
color={priceColor}
textShadow={priceGlow}
lineHeight="1"
>
{formatPrice(quoteData.currentPrice)}
</Text>
<Badge
bg={priceBg}
color={priceColor}
fontSize="20px"
fontWeight="bold"
px={4}
py={2}
borderRadius={T.radiusMD}
boxShadow={priceGlow}
>
{formatChangePercent(quoteData.changePercent)}
</Badge>
</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>
{/* ========== 数据区块Bento Grid========== */}
<Flex gap={5} flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
{/* 关键指标 */}
<GlassSection title="关键指标" flex={1}>
<VStack align="stretch" spacing={2.5}>
<MetricRow
label="市盈率 (PE)"
value={quoteData.pe ? quoteData.pe.toFixed(2) : '-'}
valueColor={T.cyan}
highlight
/>
<MetricRow
label="流通市值"
value={quoteData.marketCap || '-'}
valueColor={T.textPrimary}
highlight
/>
<MetricRow
label="发行总股本"
value={quoteData.totalShares ? `${quoteData.totalShares}亿股` : '-'}
/>
<MetricRow
label="流通股本"
value={quoteData.floatShares ? `${quoteData.floatShares}亿股` : '-'}
/>
<MetricRow
label="换手率"
value={quoteData.turnoverRate !== undefined ? `${quoteData.turnoverRate.toFixed(2)}%` : '-'}
valueColor={quoteData.turnoverRate && quoteData.turnoverRate > 5 ? T.orange : T.textWhite}
/>
<MetricRow
label="52周波动"
value={`${formatPrice(quoteData.week52Low)} - ${formatPrice(quoteData.week52High)}`}
/>
</VStack>
</GlassSection>
{/* 主力动态 */}
<GlassSection title="主力动态" flex={1}>
<VStack align="stretch" spacing={3}>
<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>
</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>
</>
)}
</VStack>
</Box>
{/* 股票对比弹窗 */}
<StockCompareModal