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

View File

@@ -1,20 +1,193 @@
/** /**
* StockQuoteCard 黑金主题配置 * StockQuoteCard 深空 FUI 主题配置
*
* 设计灵感:
* - Ash Thorp / Linear.app 科幻 FUI 风格
* - James Turrell 光影艺术
* - Glassmorphism 玻璃态设计
* - 深空漂浮的数据终端
*/ */
export const STOCK_CARD_THEME = { // ============================================
// 背景和边框 // 深空 FUI 主题系统
cardBg: '#1A202C', // ============================================
borderColor: '#C9A961', 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)',
// 文字颜色 // === Glassmorphism 效果 ===
labelColor: '#C9A961', blur: '24px',
valueColor: '#F4D03F', blurLight: '12px',
sectionTitleColor: '#F4D03F',
// 涨跌颜色(红涨绿跌) // === 边框系统 ===
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; } 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; export type StockCardTheme = typeof STOCK_CARD_THEME;

View File

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