更新Company页面的UI为FUI风格
This commit is contained in:
15
app.py
15
app.py
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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周最高
|
||||
|
||||
|
||||
Reference in New Issue
Block a user