更新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) => ( {/* 顶部金色光条 */}
<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 <Tab
key={tab.key} key={tab.key}
color={theme.tabUnselectedColor} color={theme.tabUnselectedColor}
borderRadius="md" borderRadius={DEEP_SPACE.radius}
px={5} px={6}
py={2.5} py={3}
fontSize="sm" fontSize="15px"
fontWeight="500" fontWeight="500"
whiteSpace="nowrap" whiteSpace="nowrap"
flexShrink={0} flexShrink={0}
border="1px solid transparent" border="1px solid transparent"
position="relative" position="relative"
letterSpacing="0.05em" letterSpacing="0.03em"
transition="all 0.25s cubic-bezier(0.4, 0, 0.2, 1)" transition={DEEP_SPACE.transition}
_before={{ _before={{
content: '""', content: '""',
position: 'absolute', position: 'absolute',
bottom: '-1px', bottom: '-1px',
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
width: '0%', width: isSelected ? '70%' : '0%',
height: '2px', height: '2px',
bg: '#D4AF37', bg: '#D4AF37',
transition: 'width 0.25s ease', borderRadius: 'full',
transition: 'width 0.3s ease',
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
}} }}
_selected={{ _selected={{
bg: theme.tabSelectedBg, bg: theme.tabSelectedBg,
color: theme.tabSelectedColor, color: theme.tabSelectedColor,
fontWeight: '700', fontWeight: '700',
boxShadow: '0 4px 16px rgba(212, 175, 55, 0.35), 0 0 20px rgba(212, 175, 55, 0.15)', boxShadow: DEEP_SPACE.glowGold,
border: '1px solid rgba(212, 175, 55, 0.6)', border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
transform: 'translateY(-1px)', transform: 'translateY(-2px)',
_before: {
width: '80%',
},
}} }}
_hover={{ _hover={{
bg: theme.tabHoverBg, bg: isSelected ? undefined : theme.tabHoverBg,
border: '1px solid rgba(212, 175, 55, 0.35)', border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`,
transform: 'translateY(-1px)', transform: 'translateY(-1px)',
_before: { }}
width: '60%', _active={{
}, transform: 'translateY(0)',
}} }}
> >
<HStack spacing={2}> <HStack spacing={2}>
{tab.icon && <Icon as={tab.icon} boxSize={4} />} {tab.icon && (
<Icon
as={tab.icon}
boxSize={4}
opacity={isSelected ? 1 : 0.7}
transition="opacity 0.2s"
/>
)}
<Text>{tab.name}</Text> <Text>{tab.name}</Text>
</HStack> </HStack>
</Tab> </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 <Box
bg={FUI.bgCard} {...decorativeElements.cornerGlow}
borderRadius="lg" top="-40px"
border="1px solid" left="-40px"
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}
/> />
{/* 顶部:图标 + 标题 + 标签 */} {/* 右下角光晕 */}
<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} <Box {...decorativeElements.gridOverlay} />
py={0.5} </>
bg="rgba(255, 255, 255, 0.1)" );
color={badgeColor}
borderRadius="sm" /**
fontWeight="500" * 加载骨架屏
*/
const LoadingSkeleton: React.FC = () => (
<Box
{...glassCardStyle.containerGold}
p={8}
> >
{badge} <GlowDecorations />
</Badge>
)} <VStack align="stretch" spacing={6} position="relative" zIndex={1}>
{rightIcon && ( {/* 头部骨架 */}
<Box ml="auto" opacity={0.5}> <Flex justify="space-between">
{rightIcon} <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>
)} <Box flex={1}>
</HStack> <Skeleton height="120px" startColor={T.bgInset} endColor={T.borderGlass} borderRadius={T.radiusLG} />
</Box>
{/* 主数值 */} </Flex>
<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>
{/* 子项目 */}
<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>
</HStack>
))}
</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,
code,
currentPrice,
changePercent,
industry,
}) => {
const isUp = changePercent >= 0;
const priceColor = isUp ? FUI.red : FUI.green;
const trendText = isUp ? '强势上涨' : '震荡下跌';
return (
<Box <Box
bg={FUI.bgCard} flex={flex}
borderRadius="lg" bg={T.bgInset}
border="1px solid" borderRadius={T.radiusLG}
borderColor={FUI.border} border={`1px solid ${T.borderGlass}`}
p={4} p={5}
position="relative" position="relative"
overflow="hidden" transition={T.transitionFast}
transition="all 0.25s ease"
_hover={{ _hover={{
bg: FUI.bgCardHover, borderColor: T.borderGoldHover,
borderColor: FUI.borderGold, bg: 'rgba(15, 18, 35, 0.6)',
transform: 'translateY(-2px)',
boxShadow: `0 8px 24px rgba(0, 0, 0, 0.3), 0 0 1px ${FUI.gold}`,
}} }}
> >
{/* 左上角光条 */} {/* 区块顶部光条 */}
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
left={0} left="20px"
w="40px" right="20px"
h="2px" height="1px"
bg={priceColor} background={`linear-gradient(90deg, transparent, ${T.gold}40, transparent)`}
opacity={0.8}
/> />
{/* 股票名称和代码 */} <Text
<HStack spacing={2} mb={3}> fontSize="13px"
<Text fontSize="lg" fontWeight="bold" color={FUI.text}> fontWeight="600"
{name} color={T.textSecondary}
</Text> mb={3}
<Badge textTransform="uppercase"
fontSize="10px" letterSpacing="0.1em"
px={2}
py={0.5}
bg="rgba(255, 255, 255, 0.1)"
color={FUI.textMuted}
borderRadius="sm"
> >
{code} {title}
</Badge>
</HStack>
{/* 价格和涨跌幅 */}
<HStack spacing={3} mb={2}>
<Text fontSize="2xl" fontWeight="bold" color={priceColor}>
{currentPrice.toFixed(2)}
</Text> </Text>
<Badge {children}
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> </Box>
); );
};
/**
* 指标行组件
*/
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="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> = ({ 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} <VStack align="stretch" spacing={6} position="relative" zIndex={1}>
{/* ========== 头部区域 ========== */}
<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>
{/* 行业标签 */}
{(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
/> />
{/* 卡片2: 交易热度 */} {/* 买卖比例条 */}
<MetricCard <Box mt={2}>
icon={Activity} <Progress
iconColor={FUI.orange} value={quoteData.buyRatio}
title="交易热度" size="sm"
badge="流动性" sx={{
mainValue={quoteData.marketCap || '-'} '& > div': {
mainColor={FUI.orange} bg: T.upColor,
subItems={[ boxShadow: T.upGlow,
{
label: '换手率',
value: quoteData.turnoverRate ? `${quoteData.turnoverRate.toFixed(2)}%` : '-',
color: (quoteData.turnoverRate || 0) > 5 ? FUI.orange : FUI.text,
}, },
{ }}
label: '52周区间', bg={T.downColor}
value: `${quoteData.week52Low?.toFixed(2) || '-'} - ${quoteData.week52High?.toFixed(2) || '-'}`, 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)`}
/> />
{/* 卡片3: 估值安全 */} <Flex gap={8} flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<MetricCard {/* 公司属性 */}
icon={DollarSign} <HStack spacing={6} flex={1} flexWrap="wrap" fontSize="14px">
iconColor={FUI.cyan} <HStack spacing={2}>
title="估值 VS 安全" <Icon as={Calendar} color={T.textMuted} boxSize={4} />
badge="便宜否" <Text color={T.textMuted}></Text>
mainValue={quoteData.pe ? quoteData.pe.toFixed(2) : '-'} <Text color={T.textWhite} fontWeight="600">
mainColor={FUI.cyan} {formatDate(basicInfo.establish_date)}
mainUnit="" </Text>
subItems={[ </HStack>
{ <HStack spacing={2}>
label: '市盈率(PE)', <Icon as={Coins} color={T.textMuted} boxSize={4} />
value: quoteData.pe ? (quoteData.pe > 50 ? '高估值' : quoteData.pe > 20 ? '合理' : '低估值') : '-', <Text color={T.textMuted}></Text>
color: quoteData.pe ? (quoteData.pe > 50 ? FUI.orange : quoteData.pe > 20 ? FUI.gold : FUI.green) : FUI.textMuted, <Text color={T.textWhite} fontWeight="600">
}, {formatRegisteredCapital(basicInfo.reg_capital)}
{ </Text>
label: '发行总股本', </HStack>
value: quoteData.totalShares ? `${quoteData.totalShares}亿股` : '-', <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>
{/* 卡片4: 股本结构 */} {/* 公司简介 */}
<MetricCard <Box
icon={AlertTriangle} flex={2}
iconColor={FUI.green} borderLeftWidth="1px"
title="股本结构" borderColor={T.divider}
badge="资金面" pl={8}
mainValue={quoteData.floatShares ? `${quoteData.floatShares}` : '-'} minW="0"
mainColor={FUI.green} >
mainUnit="亿股" <Text fontSize="14px" color={T.textMuted} noOfLines={2}>
subItems={[ <Text as="span" fontWeight="700" color={T.textSecondary}>
{
label: '流通股本', </Text>
value: quoteData.floatShares ? `${quoteData.floatShares}亿股` : '-', {basicInfo.company_intro || '暂无'}
}, </Text>
{ </Box>
label: '行业', </Flex>
value: quoteData.industry || '-', </>
}, )}
]} </VStack>
/> </Box>
</Grid>
{/* 股票对比弹窗 */} {/* 股票对比弹窗 */}
<StockCompareModal <StockCompareModal