更新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

@@ -1,103 +1,254 @@
/**
* StockQuoteCard - 股票行情卡片组件
*
* 展示股票的实时行情、关键指标和主力动态
* 采用 FUI 科幻风格设计 - Ash Thorp / Linear.app 风格
*
* 优化数据获取已下沉到组件内部Props 从 11 个精简为 4 个
* 采用四卡片布局 FUI 风格设计
* 参考:交易热度、估值安全、情绪风险等分区展示
*/
import React from 'react';
import {
Box,
Flex,
Grid,
VStack,
HStack,
Skeleton,
Text,
Badge,
Icon,
useDisclosure,
} from '@chakra-ui/react';
import { TrendingUp, Activity, DollarSign, AlertTriangle } from 'lucide-react';
import {
StockHeader,
PriceDisplay,
SecondaryQuote,
KeyMetrics,
MainForceInfo,
CompanyInfo,
StockCompareModal,
} from './components';
import { StockCompareModal } from './components';
import { useStockQuoteData, useStockCompare } from './hooks';
import type { StockQuoteCardProps } from './types';
// FUI 主题色彩
const FUI_THEME = {
const FUI = {
gold: '#D4AF37',
goldLight: 'rgba(212, 175, 55, 0.15)',
goldGlow: 'rgba(212, 175, 55, 0.4)',
bgCard: 'linear-gradient(145deg, rgba(26, 26, 46, 0.95) 0%, rgba(15, 15, 26, 0.98) 100%)',
border: 'rgba(212, 175, 55, 0.2)',
borderHover: 'rgba(212, 175, 55, 0.4)',
textPrimary: 'rgba(255, 255, 255, 0.95)',
textSecondary: 'rgba(255, 255, 255, 0.7)',
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)',
};
// FUI 角落装饰组件
const CornerDecoration: React.FC<{ position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' }> = ({ position }) => {
const positionStyles = {
topLeft: { top: '8px', left: '8px', borderTop: '2px solid', borderLeft: '2px solid' },
topRight: { top: '8px', right: '8px', borderTop: '2px solid', borderRight: '2px solid' },
bottomLeft: { bottom: '8px', left: '8px', borderBottom: '2px solid', borderLeft: '2px solid' },
bottomRight: { bottom: '8px', right: '8px', borderBottom: '2px solid', borderRight: '2px solid' },
};
// 单个指标卡片组件
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 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 (
<Box
position="absolute"
w="12px"
h="12px"
borderColor={FUI_THEME.goldGlow}
opacity={0.7}
{...positionStyles[position]}
/>
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={priceColor}
opacity={0.8}
/>
{/* 股票名称和代码 */}
<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>
{/* 价格和涨跌幅 */}
<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>
);
};
// FUI 卡片标题组件
const FUICardTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Text
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}
</Text>
);
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
stockCode,
isInWatchlist = false,
isWatchlistLoading = false,
onWatchlistToggle,
}) => {
// 内部获取行情数据和基本信息
const { quoteData, basicInfo, isLoading } = useStockQuoteData(stockCode);
// 内部管理股票对比逻辑
const {
currentStockInfo,
compareStockInfo,
@@ -106,185 +257,136 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
clearCompare,
} = useStockCompare(stockCode);
// 对比弹窗控制
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
// 处理对比按钮点击
const handleCompare = (compareCode: string) => {
triggerCompare(compareCode);
openCompareModal();
};
// 处理关闭对比弹窗
const handleCloseCompare = () => {
closeCompareModal();
clearCompare();
};
// 加载中或无数据时显示 FUI 风格骨架屏
// 加载中骨架屏
if (isLoading || !quoteData) {
return (
<Box
position="relative"
bg={FUI_THEME.bgCard}
borderRadius="lg"
border="1px solid"
borderColor={FUI_THEME.border}
p={6}
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" />
<CornerDecoration position="topRight" />
<CornerDecoration position="bottomLeft" />
<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>
<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) => {
if (!val) return '-';
if (val >= 100000000) return `${(val / 100000000).toFixed(2)}亿`;
if (val >= 10000) return `${(val / 10000).toFixed(0)}`;
return val.toString();
};
return (
<Box
position="relative"
bg={FUI_THEME.bgCard}
borderRadius="lg"
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
<>
<Grid templateColumns="repeat(4, 1fr)" gap={4}>
{/* 卡片1: 股票基本信息 */}
<StockInfoCard
name={quoteData.name}
code={quoteData.code}
industryL1={quoteData.industryL1}
currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
industry={quoteData.industry}
indexTags={quoteData.indexTags}
updateTime={quoteData.updateTime}
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={onWatchlistToggle}
isCompareLoading={isCompareLoading}
onCompare={handleCompare}
/>
{/* 股票对比弹窗 */}
<StockCompareModal
isOpen={isCompareModalOpen}
onClose={handleCloseCompare}
currentStock={quoteData.code}
currentStockInfo={currentStockInfo || null}
compareStock={compareStockInfo?.stock_code || ''}
compareStockInfo={compareStockInfo || null}
isLoading={isCompareLoading}
{/* 卡片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) || '-'}`,
},
]}
/>
{/* 分隔线 */}
<Box
my={4}
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.border}, transparent)`}
{/* 卡片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}亿股` : '-',
},
]}
/>
{/* 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>
{/* 卡片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>
{/* 右栏:关键指标 + 主力动态 (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}
{/* 股票对比弹窗 */}
<StockCompareModal
isOpen={isCompareModalOpen}
onClose={handleCloseCompare}
currentStock={quoteData.code}
currentStockInfo={currentStockInfo || null}
compareStock={compareStockInfo?.stock_code || ''}
compareStockInfo={compareStockInfo || null}
isLoading={isCompareLoading}
/>
</Box>
</>
);
};