更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-17 22:22:44 +08:00
parent 0d150f7b26
commit 3adff89995
6 changed files with 228 additions and 71 deletions

15
app.py
View File

@@ -8734,6 +8734,9 @@ def get_stock_quote_detail(stock_code):
if trade_result:
row = row_to_dict(trade_result)
# 调试日志:打印所有字段
app.logger.info(f"[quote-detail] stock={base_code}, row keys={list(row.keys())}")
app.logger.info(f"[quote-detail] total_shares={row.get('total_shares')}, float_shares={row.get('float_shares')}, pe_ratio={row.get('pe_ratio')}")
result_data['name'] = row.get('SECNAME') or ''
result_data['current_price'] = float(row.get('close_price') or 0)
result_data['change_percent'] = float(row.get('change_pct') or 0)
@@ -8741,17 +8744,19 @@ def get_stock_quote_detail(stock_code):
result_data['yesterday_close'] = float(row.get('pre_close') or 0)
result_data['today_high'] = float(row.get('high') or 0)
result_data['today_low'] = float(row.get('low') or 0)
result_data['pe'] = float(row.get('pe_ratio') or 0) if row.get('pe_ratio') else None
pe_value = row.get('pe_ratio') or row.get('F026N')
result_data['pe'] = float(pe_value) if pe_value else None
result_data['turnover_rate'] = float(row.get('turnover_rate') or 0)
result_data['sw_industry_l1'] = row.get('sw_industry_l1') or ''
result_data['sw_industry_l2'] = row.get('sw_industry_l2') or ''
result_data['industry_l1'] = row.get('industry_l1') or ''
result_data['industry'] = row.get('sw_industry_l2') or row.get('sw_industry_l1') or ''
# 计算股本和市值
total_shares = float(row.get('total_shares') or 0)
float_shares = float(row.get('float_shares') or 0)
close_price = float(row.get('close_price') or 0)
# 计算股本和市值(兼容别名和原始字段名)
total_shares = float(row.get('total_shares') or row.get('F020N') or 0)
float_shares = float(row.get('float_shares') or row.get('F021N') or 0)
close_price = float(row.get('close_price') or row.get('F007N') or 0)
app.logger.info(f"[quote-detail] calculated: total_shares={total_shares}, float_shares={float_shares}")
# 发行总股本(亿股)
if total_shares > 0:

View File

@@ -61,11 +61,11 @@ export interface SubTabTheme {
const THEME_PRESETS: Record<string, SubTabTheme> = {
blackGold: {
bg: 'transparent',
borderColor: 'rgba(212, 175, 55, 0.2)',
tabSelectedBg: 'linear-gradient(135deg, #D4AF37 0%, #B8960C 100%)',
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(212, 175, 55, 0.8)',
tabHoverBg: 'rgba(212, 175, 55, 0.1)',
tabUnselectedColor: 'rgba(212, 175, 55, 0.75)',
tabHoverBg: 'rgba(212, 175, 55, 0.12)',
},
default: {
bg: 'white',
@@ -162,11 +162,11 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
bg={theme.bg}
borderBottom="1px solid"
borderColor={theme.borderColor}
pl={0}
pr={2}
py={1.5}
pl={2}
pr={4}
py={3}
flexWrap="nowrap"
gap={1}
gap={2}
alignItems="center"
overflowX="auto"
css={{
@@ -178,29 +178,51 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
<Tab
key={tab.key}
color={theme.tabUnselectedColor}
borderRadius="full"
px={3}
py={1.5}
fontSize="xs"
borderRadius="md"
px={5}
py={2.5}
fontSize="sm"
fontWeight="500"
whiteSpace="nowrap"
flexShrink={0}
border="1px solid transparent"
transition="all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
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: 'bold',
boxShadow: '0 0 12px rgba(212, 175, 55, 0.4)',
border: '1px solid rgba(212, 175, 55, 0.5)',
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.3)',
border: '1px solid rgba(212, 175, 55, 0.35)',
transform: 'translateY(-1px)',
_before: {
width: '60%',
},
}}
>
<HStack spacing={1.5}>
{tab.icon && <Icon as={tab.icon} boxSize={3.5} />}
<Text letterSpacing="wide">{tab.name}</Text>
<HStack spacing={2}>
{tab.icon && <Icon as={tab.icon} boxSize={4} />}
<Text>{tab.name}</Text>
</HStack>
</Tab>
))}

View File

@@ -10,22 +10,20 @@ import { STOCK_CARD_THEME } from './theme';
export interface KeyMetricsProps {
pe: number;
eps?: number;
pb: number;
marketCap: string;
totalShares?: number; // 发行总股本(亿股)
floatShares?: number; // 流通股本(亿股)
turnoverRate?: number; // 换手率(%
week52Low: number;
week52High: number;
}
export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
pe,
eps,
pb,
marketCap,
totalShares,
floatShares,
turnoverRate,
week52Low,
week52High,
}) => {
@@ -45,25 +43,13 @@ export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
<HStack justify="space-between">
<Text color={labelColor}>(PE)</Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{pe.toFixed(2)}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}>(EPS)</Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{eps?.toFixed(3) || '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}>(PB)</Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{pb.toFixed(2)}
{pe ? pe.toFixed(2) : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{marketCap}
{marketCap || '-'}
</Text>
</HStack>
<HStack justify="space-between">
@@ -72,6 +58,18 @@ export const KeyMetrics: React.FC<KeyMetricsProps> = memo(({
{totalShares ? `${totalShares}亿股` : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{floatShares ? `${floatShares}亿股` : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}></Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">
{turnoverRate !== undefined ? `${turnoverRate.toFixed(2)}%` : '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text color={labelColor}>52</Text>
<Text color={valueColor} fontWeight="bold" fontSize="16px">

View File

@@ -35,11 +35,10 @@ const transformQuoteData = (apiData: any, stockCode: string): StockQuoteCardData
// 关键指标
pe: apiData.pe || apiData.pe_ttm || 0,
eps: apiData.eps || apiData.basic_eps || undefined,
pb: apiData.pb || apiData.pb_mrq || 0,
marketCap: apiData.market_cap || apiData.marketCap || apiData.circ_mv || '0',
totalShares: apiData.total_shares || apiData.totalShares || undefined,
floatShares: apiData.float_shares || apiData.floatShares || undefined,
turnoverRate: apiData.turnover_rate || apiData.turnoverRate || undefined,
week52Low: apiData.week52_low || apiData.week52Low || 0,
week52High: apiData.week52_high || apiData.week52High || 0,

View File

@@ -2,7 +2,7 @@
* StockQuoteCard - 股票行情卡片组件
*
* 展示股票的实时行情、关键指标和主力动态
* 采用原子组件拆分,提高可维护性和复用性
* 采用 FUI 科幻风格设计 - Ash Thorp / Linear.app 风格
*
* 优化数据获取已下沉到组件内部Props 从 11 个精简为 4 个
*/
@@ -10,11 +10,10 @@
import React from 'react';
import {
Box,
Card,
CardBody,
Flex,
VStack,
Skeleton,
Text,
useDisclosure,
} from '@chakra-ui/react';
@@ -26,11 +25,69 @@ import {
MainForceInfo,
CompanyInfo,
StockCompareModal,
STOCK_CARD_THEME,
} from './components';
import { useStockQuoteData, useStockCompare } from './hooks';
import type { StockQuoteCardProps } from './types';
// FUI 主题色彩
const FUI_THEME = {
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)',
};
// 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' },
};
return (
<Box
position="absolute"
w="12px"
h="12px"
borderColor={FUI_THEME.goldGlow}
opacity={0.7}
{...positionStyles[position]}
/>
);
};
// 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,
@@ -64,26 +121,70 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
clearCompare();
};
const { cardBg, borderColor } = STOCK_CARD_THEME;
// 加载中或无数据时显示骨架屏
// 加载中或无数据时显示 FUI 风格骨架屏
if (isLoading || !quoteData) {
return (
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
<CardBody>
<VStack spacing={4} align="stretch">
<Skeleton height="30px" width="200px" />
<Skeleton height="60px" />
<Skeleton height="80px" />
</VStack>
</CardBody>
</Card>
<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>
);
}
return (
<Card bg={cardBg} shadow="sm" borderWidth="1px" borderColor={borderColor}>
<CardBody>
<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
name={quoteData.name}
@@ -110,6 +211,13 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
isLoading={isCompareLoading}
/>
{/* 分隔线 */}
<Box
my={4}
height="1px"
bg={`linear-gradient(90deg, transparent, ${FUI_THEME.border}, transparent)`}
/>
{/* 1:2 布局 */}
<Flex gap={8}>
{/* 左栏:价格信息 (flex=1) */}
@@ -127,14 +235,20 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
</Box>
{/* 右栏:关键指标 + 主力动态 (flex=2) */}
<Flex flex="2" minWidth="0" gap={8} borderLeftWidth="1px" borderColor={borderColor} pl={8}>
<Flex
flex="2"
minWidth="0"
gap={8}
borderLeftWidth="1px"
borderColor={FUI_THEME.border}
pl={8}
>
<KeyMetrics
pe={quoteData.pe}
eps={quoteData.eps}
pb={quoteData.pb}
marketCap={quoteData.marketCap}
totalShares={quoteData.totalShares}
floatShares={quoteData.floatShares}
turnoverRate={quoteData.turnoverRate}
week52Low={quoteData.week52Low}
week52High={quoteData.week52High}
/>
@@ -148,9 +262,29 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
</Flex>
{/* 公司信息区块 */}
{basicInfo && <CompanyInfo basicInfo={basicInfo} />}
</CardBody>
</Card>
{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>
);
};

View File

@@ -26,11 +26,10 @@ export interface StockQuoteCardData {
// 关键指标
pe: number; // 市盈率
eps?: number; // 每股收益
pb: number; // 市净率
marketCap: string; // 流通市值(已格式化,如 "2.73万亿"
totalShares?: number; // 发行总股本(亿股)
floatShares?: number; // 流通股本(亿股)
turnoverRate?: number; // 换手率(%
week52Low: number; // 52周最低
week52High: number; // 52周最高