更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-17 22:30:18 +08:00
parent 3adff89995
commit 0214052965
2 changed files with 319 additions and 217 deletions

View File

@@ -64,8 +64,8 @@ const THEME_PRESETS: Record<string, SubTabTheme> = {
borderColor: 'rgba(212, 175, 55, 0.15)', 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%)', tabSelectedBg: 'linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(184, 150, 12, 0.95) 100%)',
tabSelectedColor: '#0A0A14', tabSelectedColor: '#0A0A14',
tabUnselectedColor: 'rgba(212, 175, 55, 0.75)', tabUnselectedColor: 'rgba(255, 255, 255, 0.85)', // 调亮:白色更清晰
tabHoverBg: 'rgba(212, 175, 55, 0.12)', tabHoverBg: 'rgba(212, 175, 55, 0.15)',
}, },
default: { default: {
bg: 'white', bg: 'white',

View File

@@ -1,92 +1,246 @@
/** /**
* StockQuoteCard - 股票行情卡片组件 * StockQuoteCard - 股票行情卡片组件
* *
* 展示股票的实时行情、关键指标和主力动态 * 采用四卡片布局 FUI 风格设计
* 采用 FUI 科幻风格设计 - Ash Thorp / Linear.app 风格 * 参考:交易热度、估值安全、情绪风险等分区展示
*
* 优化数据获取已下沉到组件内部Props 从 11 个精简为 4 个
*/ */
import React from 'react'; import React from 'react';
import { import {
Box, Box,
Flex, Flex,
Grid,
VStack, VStack,
HStack,
Skeleton, Skeleton,
Text, Text,
Badge,
Icon,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { TrendingUp, Activity, DollarSign, AlertTriangle } from 'lucide-react';
import { import { StockCompareModal } from './components';
StockHeader,
PriceDisplay,
SecondaryQuote,
KeyMetrics,
MainForceInfo,
CompanyInfo,
StockCompareModal,
} from './components';
import { useStockQuoteData, useStockCompare } from './hooks'; import { useStockQuoteData, useStockCompare } from './hooks';
import type { StockQuoteCardProps } from './types'; import type { StockQuoteCardProps } from './types';
// FUI 主题色彩 // FUI 主题色彩
const FUI_THEME = { const FUI = {
gold: '#D4AF37', gold: '#D4AF37',
goldLight: 'rgba(212, 175, 55, 0.15)', orange: '#FF6B35',
goldGlow: 'rgba(212, 175, 55, 0.4)', green: '#00D984',
bgCard: 'linear-gradient(145deg, rgba(26, 26, 46, 0.95) 0%, rgba(15, 15, 26, 0.98) 100%)', red: '#FF4757',
border: 'rgba(212, 175, 55, 0.2)', cyan: '#00D4FF',
borderHover: 'rgba(212, 175, 55, 0.4)', purple: '#A855F7',
textPrimary: 'rgba(255, 255, 255, 0.95)', bgCard: 'rgba(20, 20, 30, 0.95)',
textSecondary: 'rgba(255, 255, 255, 0.7)', 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)',
}; };
// FUI 角落装饰组件 // 单个指标卡片组件
const CornerDecoration: React.FC<{ position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' }> = ({ position }) => { interface MetricCardProps {
const positionStyles = { icon: React.ElementType;
topLeft: { top: '8px', left: '8px', borderTop: '2px solid', borderLeft: '2px solid' }, iconColor: string;
topRight: { top: '8px', right: '8px', borderTop: '2px solid', borderRight: '2px solid' }, title: string;
bottomLeft: { bottom: '8px', left: '8px', borderBottom: '2px solid', borderLeft: '2px solid' }, badge?: string;
bottomRight: { bottom: '8px', right: '8px', borderBottom: '2px solid', borderRight: '2px solid' }, badgeColor?: string;
}; 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
position="absolute"
top={0}
left={0}
w="40px"
h="2px"
bg={iconColor}
opacity={0.8}
/>
{/* 顶部:图标 + 标题 + 标签 */}
<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>
{/* 主数值 */}
<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>
</Box>
);
// 股票信息卡片(第一个)
interface StockInfoCardProps {
name: string;
code: string;
currentPrice: number;
changePercent: number;
industry?: 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 ( return (
<Box <Box
position="absolute" bg={FUI.bgCard}
w="12px" borderRadius="lg"
h="12px" border="1px solid"
borderColor={FUI_THEME.goldGlow} borderColor={FUI.border}
opacity={0.7} p={4}
{...positionStyles[position]} position="relative"
/> overflow="hidden"
); transition="all 0.25s ease"
}; _hover={{
bg: FUI.bgCardHover,
// FUI 卡片标题组件 borderColor: FUI.borderGold,
const FUICardTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => ( transform: 'translateY(-2px)',
<Text boxShadow: `0 8px 24px rgba(0, 0, 0, 0.3), 0 0 1px ${FUI.gold}`,
fontSize="11px"
fontWeight="600"
letterSpacing="0.15em"
textTransform="uppercase"
color={FUI_THEME.gold}
mb={4}
display="flex"
alignItems="center"
_before={{
content: '""',
display: 'inline-block',
width: '4px',
height: '4px',
bg: FUI_THEME.gold,
borderRadius: '1px',
mr: 2,
boxShadow: `0 0 6px ${FUI_THEME.gold}`,
}} }}
> >
{children} {/* 左上角光条 */}
<Box
position="absolute"
top={0}
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}
</Text> </Text>
<Badge
fontSize="10px"
px={2}
py={0.5}
bg="rgba(255, 255, 255, 0.1)"
color={FUI.textMuted}
borderRadius="sm"
>
{code}
</Badge>
</HStack>
{/* 价格和涨跌幅 */}
<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 StockQuoteCard: React.FC<StockQuoteCardProps> = ({ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
stockCode, stockCode,
@@ -94,10 +248,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
isWatchlistLoading = false, isWatchlistLoading = false,
onWatchlistToggle, onWatchlistToggle,
}) => { }) => {
// 内部获取行情数据和基本信息
const { quoteData, basicInfo, isLoading } = useStockQuoteData(stockCode); const { quoteData, basicInfo, isLoading } = useStockQuoteData(stockCode);
// 内部管理股票对比逻辑
const { const {
currentStockInfo, currentStockInfo,
compareStockInfo, compareStockInfo,
@@ -106,100 +257,125 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
clearCompare, clearCompare,
} = useStockCompare(stockCode); } = useStockCompare(stockCode);
// 对比弹窗控制
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure(); const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
// 处理对比按钮点击
const handleCompare = (compareCode: string) => { const handleCompare = (compareCode: string) => {
triggerCompare(compareCode); triggerCompare(compareCode);
openCompareModal(); openCompareModal();
}; };
// 处理关闭对比弹窗
const handleCloseCompare = () => { const handleCloseCompare = () => {
closeCompareModal(); closeCompareModal();
clearCompare(); clearCompare();
}; };
// 加载中或无数据时显示 FUI 风格骨架屏 // 加载中骨架屏
if (isLoading || !quoteData) { if (isLoading || !quoteData) {
return ( return (
<Grid templateColumns="repeat(4, 1fr)" gap={4}>
{[1, 2, 3, 4].map((i) => (
<Box <Box
position="relative" key={i}
bg={FUI_THEME.bgCard} bg={FUI.bgCard}
borderRadius="lg" borderRadius="lg"
border="1px solid" border="1px solid"
borderColor={FUI_THEME.border} borderColor={FUI.border}
p={6} p={4}
overflow="hidden"
boxShadow={`0 4px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)`}
> >
<CornerDecoration position="topLeft" /> <Skeleton height="20px" width="120px" mb={3} startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
<CornerDecoration position="topRight" /> <Skeleton height="32px" width="80px" mb={2} startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
<CornerDecoration position="bottomLeft" /> <Skeleton height="14px" width="100%" startColor="rgba(255,255,255,0.05)" endColor="rgba(255,255,255,0.1)" />
<CornerDecoration position="bottomRight" />
<VStack spacing={4} align="stretch">
<Skeleton height="30px" width="200px" startColor="rgba(212, 175, 55, 0.1)" endColor="rgba(212, 175, 55, 0.2)" />
<Skeleton height="60px" startColor="rgba(212, 175, 55, 0.1)" endColor="rgba(212, 175, 55, 0.2)" />
<Skeleton height="80px" startColor="rgba(212, 175, 55, 0.1)" endColor="rgba(212, 175, 55, 0.2)" />
</VStack>
</Box> </Box>
))}
</Grid>
); );
} }
// 格式化数值
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();
};
return ( return (
<Box <>
position="relative" <Grid templateColumns="repeat(4, 1fr)" gap={4}>
bg={FUI_THEME.bgCard} {/* 卡片1: 股票基本信息 */}
borderRadius="lg" <StockInfoCard
border="1px solid"
borderColor={FUI_THEME.border}
overflow="hidden"
boxShadow={`0 4px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)`}
backdropFilter="blur(12px)"
transition="all 0.3s ease"
_hover={{
borderColor: FUI_THEME.borderHover,
boxShadow: `0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px ${FUI_THEME.goldLight}, inset 0 1px 0 rgba(255, 255, 255, 0.08)`,
}}
>
{/* FUI 角落装饰 */}
<CornerDecoration position="topLeft" />
<CornerDecoration position="topRight" />
<CornerDecoration position="bottomLeft" />
<CornerDecoration position="bottomRight" />
{/* 顶部光效线条 */}
<Box
position="absolute"
top={0}
left="20%"
right="20%"
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.gold}, transparent)`}
opacity={0.6}
/>
{/* 内容区域 */}
<Box p={6}>
{/* FUI 标题 */}
<FUICardTitle> · STOCK QUOTE</FUICardTitle>
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
<StockHeader
name={quoteData.name} name={quoteData.name}
code={quoteData.code} code={quoteData.code}
industryL1={quoteData.industryL1} currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
industry={quoteData.industry} industry={quoteData.industry}
indexTags={quoteData.indexTags}
updateTime={quoteData.updateTime}
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={onWatchlistToggle}
isCompareLoading={isCompareLoading}
onCompare={handleCompare}
/> />
{/* 卡片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) || '-'}`,
},
]}
/>
{/* 卡片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}亿股` : '-',
},
]}
/>
{/* 卡片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>
{/* 股票对比弹窗 */} {/* 股票对比弹窗 */}
<StockCompareModal <StockCompareModal
isOpen={isCompareModalOpen} isOpen={isCompareModalOpen}
@@ -210,81 +386,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
compareStockInfo={compareStockInfo || null} compareStockInfo={compareStockInfo || null}
isLoading={isCompareLoading} isLoading={isCompareLoading}
/> />
{/* 分隔线 */}
<Box
my={4}
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.border}, transparent)`}
/>
{/* 1:2 布局 */}
<Flex gap={8}>
{/* 左栏:价格信息 (flex=1) */}
<Box flex="1" minWidth="0">
<PriceDisplay
currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
/>
<SecondaryQuote
todayOpen={quoteData.todayOpen}
yesterdayClose={quoteData.yesterdayClose}
todayHigh={quoteData.todayHigh}
todayLow={quoteData.todayLow}
/>
</Box>
{/* 右栏:关键指标 + 主力动态 (flex=2) */}
<Flex
flex="2"
minWidth="0"
gap={8}
borderLeftWidth="1px"
borderColor={FUI_THEME.border}
pl={8}
>
<KeyMetrics
pe={quoteData.pe}
marketCap={quoteData.marketCap}
totalShares={quoteData.totalShares}
floatShares={quoteData.floatShares}
turnoverRate={quoteData.turnoverRate}
week52Low={quoteData.week52Low}
week52High={quoteData.week52High}
/>
<MainForceInfo
mainNetInflow={quoteData.mainNetInflow}
institutionHolding={quoteData.institutionHolding}
buyRatio={quoteData.buyRatio}
sellRatio={quoteData.sellRatio}
/>
</Flex>
</Flex>
{/* 公司信息区块 */}
{basicInfo && (
<>
<Box
my={4}
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.border}, transparent)`}
/>
<CompanyInfo basicInfo={basicInfo} />
</> </>
)}
</Box>
{/* 底部光效线条 */}
<Box
position="absolute"
bottom={0}
left="30%"
right="30%"
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.goldGlow}, transparent)`}
opacity={0.4}
/>
</Box>
); );
}; };