update pay ui
This commit is contained in:
@@ -3,63 +3,77 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { AutoComplete, Spin } from 'antd';
|
import { AutoComplete, Spin } from 'antd';
|
||||||
import { useStockSearch } from '@hooks/useStockSearch';
|
import { useStockSearch } from '@hooks/useStockSearch';
|
||||||
import {
|
import {
|
||||||
Container,
|
Box,
|
||||||
Heading,
|
Flex,
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
HStack,
|
HStack,
|
||||||
VStack,
|
VStack,
|
||||||
Button,
|
|
||||||
Text,
|
Text,
|
||||||
Badge,
|
Button,
|
||||||
Divider,
|
|
||||||
Icon,
|
Icon,
|
||||||
useColorModeValue,
|
Badge,
|
||||||
useColorMode,
|
|
||||||
IconButton,
|
|
||||||
useToast,
|
useToast,
|
||||||
|
Skeleton,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { SearchIcon, MoonIcon, SunIcon, StarIcon } from '@chakra-ui/icons';
|
import { Search, Star, Building2, TrendingUp, Wallet, FileBarChart } from 'lucide-react';
|
||||||
import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-icons/fa';
|
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getApiBase } from '../../utils/apiConfig';
|
import { getApiBase } from '../../utils/apiConfig';
|
||||||
|
|
||||||
|
// Tab 内容组件
|
||||||
import FinancialPanorama from './components/FinancialPanorama';
|
import FinancialPanorama from './components/FinancialPanorama';
|
||||||
import ForecastReport from './ForecastReport';
|
import ForecastReport from './ForecastReport';
|
||||||
import MarketDataView from './MarketDataView';
|
import MarketDataView from './MarketDataView';
|
||||||
import CompanyOverview from './components/CompanyOverview';
|
import CompanyOverview from './components/CompanyOverview';
|
||||||
// 导入 PostHog 追踪 Hook
|
|
||||||
|
// PostHog 追踪
|
||||||
import { useCompanyEvents } from './hooks/useCompanyEvents';
|
import { useCompanyEvents } from './hooks/useCompanyEvents';
|
||||||
|
|
||||||
// 页面组件
|
// 通用组件
|
||||||
import CompanyHeader from './components/CompanyHeader';
|
import SubTabContainer from '@components/SubTabContainer';
|
||||||
import StockQuoteCard from './components/StockQuoteCard';
|
|
||||||
import CompanyTabs from './components/CompanyTabs';
|
// ============================================
|
||||||
|
// 黑金主题配置
|
||||||
|
// ============================================
|
||||||
|
const THEME = {
|
||||||
|
bg: '#1A1A2E', // 深蓝黑背景
|
||||||
|
cardBg: '#16213E', // 卡片背景
|
||||||
|
gold: '#D4AF37', // 金色
|
||||||
|
goldLight: '#F0D78C', // 浅金色
|
||||||
|
goldDark: '#B8960C', // 深金色
|
||||||
|
textPrimary: '#FFFFFF',
|
||||||
|
textSecondary: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
textMuted: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
border: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
borderHover: 'rgba(212, 175, 55, 0.4)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Tab 配置
|
||||||
|
// ============================================
|
||||||
|
const TAB_CONFIG = [
|
||||||
|
{ key: 'overview', name: '公司概览', icon: Building2, component: CompanyOverview },
|
||||||
|
{ key: 'market', name: '股票行情', icon: TrendingUp, component: MarketDataView },
|
||||||
|
{ key: 'financial', name: '财务全景', icon: Wallet, component: FinancialPanorama },
|
||||||
|
{ key: 'forecast', name: '盈利预测', icon: FileBarChart, component: ForecastReport },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公司详情页面
|
* 公司详情页面
|
||||||
*
|
*
|
||||||
* 功能:
|
* 使用黑金主题,紧凑布局
|
||||||
* - 股票搜索与代码管理
|
|
||||||
* - 自选股添加/移除
|
|
||||||
* - 多维度数据展示(概览、行情、财务、预测)
|
|
||||||
* - PostHog 事件追踪
|
|
||||||
*/
|
*/
|
||||||
const CompanyIndex = () => {
|
const CompanyIndex = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [stockCode, setStockCode] = useState(searchParams.get('scode') || '000001');
|
const [stockCode, setStockCode] = useState(searchParams.get('scode') || '000001');
|
||||||
const [inputCode, setInputCode] = useState(stockCode);
|
const [inputCode, setInputCode] = useState(stockCode);
|
||||||
|
const [stockName, setStockName] = useState('');
|
||||||
|
const [stockPrice, setStockPrice] = useState(null);
|
||||||
|
const [priceChange, setPriceChange] = useState(null);
|
||||||
const prevStockCodeRef = useRef(stockCode);
|
const prevStockCodeRef = useRef(stockCode);
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
// 🎯 PostHog 事件追踪
|
// PostHog 事件追踪
|
||||||
const {
|
const {
|
||||||
trackStockSearched,
|
trackStockSearched,
|
||||||
trackTabChanged,
|
trackTabChanged,
|
||||||
@@ -67,7 +81,7 @@ const CompanyIndex = () => {
|
|||||||
trackWatchlistRemoved,
|
trackWatchlistRemoved,
|
||||||
} = useCompanyEvents({ stockCode });
|
} = useCompanyEvents({ stockCode });
|
||||||
|
|
||||||
// 🔍 股票搜索 Hook(支持代码、名称、拼音缩写)
|
// 股票搜索 Hook
|
||||||
const {
|
const {
|
||||||
searchResults,
|
searchResults,
|
||||||
isSearching,
|
isSearching,
|
||||||
@@ -84,28 +98,26 @@ const CompanyIndex = () => {
|
|||||||
return searchResults.map((stock) => ({
|
return searchResults.map((stock) => ({
|
||||||
value: stock.stock_code,
|
value: stock.stock_code,
|
||||||
label: (
|
label: (
|
||||||
<span>
|
<Flex justify="space-between" align="center" py={1}>
|
||||||
<strong>{stock.stock_code}</strong> {stock.stock_name}
|
<HStack spacing={2}>
|
||||||
|
<Text fontWeight="bold" color={THEME.gold}>{stock.stock_code}</Text>
|
||||||
|
<Text color={THEME.textPrimary}>{stock.stock_name}</Text>
|
||||||
|
</HStack>
|
||||||
{stock.pinyin_abbr && (
|
{stock.pinyin_abbr && (
|
||||||
<span style={{ color: '#999', marginLeft: 8 }}>
|
<Text fontSize="xs" color={THEME.textMuted}>
|
||||||
({stock.pinyin_abbr.toUpperCase()})
|
{stock.pinyin_abbr.toUpperCase()}
|
||||||
</span>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</span>
|
</Flex>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}, [searchResults]);
|
}, [searchResults]);
|
||||||
|
|
||||||
// Tab 索引状态(用于追踪 Tab 切换)
|
// 自选股状态
|
||||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
|
||||||
|
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
|
||||||
const tabBg = useColorModeValue('gray.50', 'gray.700');
|
|
||||||
const activeBg = useColorModeValue('blue.500', 'blue.400');
|
|
||||||
|
|
||||||
const [isInWatchlist, setIsInWatchlist] = useState(false);
|
const [isInWatchlist, setIsInWatchlist] = useState(false);
|
||||||
const [isWatchlistLoading, setIsWatchlistLoading] = useState(false);
|
const [isWatchlistLoading, setIsWatchlistLoading] = useState(false);
|
||||||
|
|
||||||
|
// 加载自选股状态
|
||||||
const loadWatchlistStatus = useCallback(async () => {
|
const loadWatchlistStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const base = getApiBase();
|
const base = getApiBase();
|
||||||
@@ -125,24 +137,44 @@ const CompanyIndex = () => {
|
|||||||
setIsInWatchlist(false);
|
setIsInWatchlist(false);
|
||||||
}
|
}
|
||||||
}, [stockCode]);
|
}, [stockCode]);
|
||||||
|
|
||||||
// 当URL参数变化时更新股票代码
|
// 加载股票基本信息
|
||||||
|
const loadStockInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
const resp = await fetch(base + `/api/financial/stock-info/${stockCode}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && data.data) {
|
||||||
|
setStockName(data.data.stock_name || '');
|
||||||
|
setStockPrice(data.data.close_price);
|
||||||
|
setPriceChange(data.data.change_pct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('CompanyIndex', 'loadStockInfo', e);
|
||||||
|
}
|
||||||
|
}, [stockCode]);
|
||||||
|
|
||||||
|
// URL 参数变化时更新股票代码
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockCode !== prevStockCodeRef.current) {
|
if (stockCode !== prevStockCodeRef.current) {
|
||||||
trackStockSearched(stockCode, prevStockCodeRef.current);
|
trackStockSearched(stockCode, prevStockCodeRef.current);
|
||||||
prevStockCodeRef.current = stockCode;
|
prevStockCodeRef.current = stockCode;
|
||||||
}
|
}
|
||||||
}, [searchParams, stockCode]);
|
}, [searchParams, stockCode, trackStockSearched]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadWatchlistStatus();
|
loadWatchlistStatus();
|
||||||
}, [loadWatchlistStatus]);
|
loadStockInfo();
|
||||||
|
}, [loadWatchlistStatus, loadStockInfo]);
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
if (inputCode && inputCode !== stockCode) {
|
if (inputCode && inputCode !== stockCode) {
|
||||||
// 🎯 追踪股票搜索
|
|
||||||
trackStockSearched(inputCode, stockCode);
|
trackStockSearched(inputCode, stockCode);
|
||||||
|
|
||||||
setStockCode(inputCode);
|
setStockCode(inputCode);
|
||||||
setSearchParams({ scode: inputCode });
|
setSearchParams({ scode: inputCode });
|
||||||
}
|
}
|
||||||
@@ -159,14 +191,13 @@ const CompanyIndex = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 自选股切换
|
||||||
const handleWatchlistToggle = async () => {
|
const handleWatchlistToggle = async () => {
|
||||||
if (!stockCode) {
|
if (!stockCode) {
|
||||||
logger.warn('CompanyIndex', 'handleWatchlistToggle', '无效的股票代码', { stockCode });
|
|
||||||
toast({ title: '无效的股票代码', status: 'error', duration: 2000 });
|
toast({ title: '无效的股票代码', status: 'error', duration: 2000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
logger.warn('CompanyIndex', 'handleWatchlistToggle', '用户未登录', { stockCode });
|
|
||||||
toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 });
|
toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,219 +205,206 @@ const CompanyIndex = () => {
|
|||||||
setIsWatchlistLoading(true);
|
setIsWatchlistLoading(true);
|
||||||
const base = getApiBase();
|
const base = getApiBase();
|
||||||
if (isInWatchlist) {
|
if (isInWatchlist) {
|
||||||
logger.debug('CompanyIndex', '准备从自选移除', { stockCode });
|
const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, {
|
||||||
const url = base + `/api/account/watchlist/${stockCode}`;
|
|
||||||
logger.api.request('DELETE', url, { stockCode });
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.api.response('DELETE', url, resp.status);
|
|
||||||
if (!resp.ok) throw new Error('删除失败');
|
if (!resp.ok) throw new Error('删除失败');
|
||||||
|
|
||||||
// 🎯 追踪移除自选
|
|
||||||
trackWatchlistRemoved(stockCode);
|
trackWatchlistRemoved(stockCode);
|
||||||
|
|
||||||
setIsInWatchlist(false);
|
setIsInWatchlist(false);
|
||||||
toast({ title: '已从自选移除', status: 'info', duration: 1500 });
|
toast({ title: '已从自选移除', status: 'info', duration: 1500 });
|
||||||
} else {
|
} else {
|
||||||
logger.debug('CompanyIndex', '准备添加到自选', { stockCode });
|
const resp = await fetch(base + '/api/account/watchlist', {
|
||||||
const url = base + '/api/account/watchlist';
|
|
||||||
const body = { stock_code: stockCode };
|
|
||||||
logger.api.request('POST', url, body);
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify({ stock_code: stockCode })
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.api.response('POST', url, resp.status);
|
|
||||||
if (!resp.ok) throw new Error('添加失败');
|
if (!resp.ok) throw new Error('添加失败');
|
||||||
|
|
||||||
// 🎯 追踪加入自选
|
|
||||||
trackWatchlistAdded(stockCode);
|
trackWatchlistAdded(stockCode);
|
||||||
|
|
||||||
setIsInWatchlist(true);
|
setIsInWatchlist(true);
|
||||||
toast({ title: '已加入自选', status: 'success', duration: 1500 });
|
toast({ title: '已加入自选', status: 'success', duration: 1500 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('CompanyIndex', 'handleWatchlistToggle', error, { stockCode, isInWatchlist });
|
logger.error('CompanyIndex', 'handleWatchlistToggle', error);
|
||||||
toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
|
toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
|
||||||
} finally {
|
} finally {
|
||||||
setIsWatchlistLoading(false);
|
setIsWatchlistLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tab 变更处理
|
||||||
|
const handleTabChange = useCallback((index, tabKey) => {
|
||||||
|
const tabNames = TAB_CONFIG.map(t => t.name);
|
||||||
|
trackTabChanged(index, tabNames[index], index);
|
||||||
|
}, [trackTabChanged]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxW="container.xl" py={5}>
|
<Box bg={THEME.bg} minH="calc(100vh - 60px)">
|
||||||
{/* 页面标题和股票搜索 */}
|
{/* 顶部搜索栏 */}
|
||||||
<VStack align="stretch" spacing={5}>
|
<Box
|
||||||
<Card bg={bgColor} shadow="md">
|
bg={THEME.cardBg}
|
||||||
<CardBody>
|
borderBottom="1px solid"
|
||||||
<HStack justify="space-between" align="center">
|
borderColor={THEME.border}
|
||||||
<VStack align="start" spacing={1}>
|
px={6}
|
||||||
<Heading size="lg">个股详情</Heading>
|
py={4}
|
||||||
<Text color="gray.600" fontSize="sm">
|
>
|
||||||
查看股票实时行情、财务数据和盈利预测
|
<Flex
|
||||||
|
maxW="container.xl"
|
||||||
|
mx="auto"
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
wrap="wrap"
|
||||||
|
gap={4}
|
||||||
|
>
|
||||||
|
{/* 左侧:股票信息 */}
|
||||||
|
<HStack spacing={4}>
|
||||||
|
{/* 股票代码和名称 */}
|
||||||
|
<VStack align="start" spacing={0}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={THEME.gold}
|
||||||
|
letterSpacing="wider"
|
||||||
|
>
|
||||||
|
{stockCode}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
{stockName && (
|
||||||
|
<Text fontSize="xl" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
<HStack spacing={3}>
|
{stockName}
|
||||||
<AutoComplete
|
</Text>
|
||||||
value={inputCode}
|
)}
|
||||||
options={stockOptions}
|
|
||||||
onSearch={doSearch}
|
|
||||||
onSelect={handleStockSelect}
|
|
||||||
onChange={(value) => setInputCode(value)}
|
|
||||||
placeholder="输入代码、名称或拼音缩写"
|
|
||||||
style={{ width: 280 }}
|
|
||||||
size="large"
|
|
||||||
notFoundContent={isSearching ? <Spin size="small" /> : null}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
size="lg"
|
|
||||||
onClick={handleSearch}
|
|
||||||
leftIcon={<SearchIcon />}
|
|
||||||
>
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme={isInWatchlist ? 'yellow' : 'teal'}
|
|
||||||
variant={isInWatchlist ? 'solid' : 'outline'}
|
|
||||||
size="lg"
|
|
||||||
onClick={handleWatchlistToggle}
|
|
||||||
leftIcon={<StarIcon />}
|
|
||||||
isLoading={isWatchlistLoading}
|
|
||||||
>
|
|
||||||
{isInWatchlist ? '已在自选' : '加入自选'}
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
|
||||||
onClick={toggleColorMode}
|
|
||||||
variant="outline"
|
|
||||||
colorScheme={colorMode === 'light' ? 'blue' : 'yellow'}
|
|
||||||
size="lg"
|
|
||||||
aria-label="Toggle color mode"
|
|
||||||
/>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
{stockPrice !== null && (
|
||||||
|
<HStack spacing={3} mt={1}>
|
||||||
{/* 当前股票信息 */}
|
<Text fontSize="lg" fontWeight="bold" color={THEME.textPrimary}>
|
||||||
<HStack mt={4} spacing={4}>
|
¥{stockPrice?.toFixed(2)}
|
||||||
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>
|
</Text>
|
||||||
股票代码: {stockCode}
|
{priceChange !== null && (
|
||||||
</Badge>
|
<Badge
|
||||||
<Text fontSize="sm" color="gray.600">
|
px={2}
|
||||||
更新时间: {new Date().toLocaleString()}
|
py={0.5}
|
||||||
</Text>
|
borderRadius="md"
|
||||||
</HStack>
|
bg={priceChange >= 0 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(34, 197, 94, 0.2)'}
|
||||||
</CardBody>
|
color={priceChange >= 0 ? '#EF4444' : '#22C55E'}
|
||||||
</Card>
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
{/* 数据展示区域 */}
|
>
|
||||||
<Card bg={bgColor} shadow="lg">
|
{priceChange >= 0 ? '+' : ''}{priceChange?.toFixed(2)}%
|
||||||
<CardBody p={0}>
|
</Badge>
|
||||||
<Tabs
|
)}
|
||||||
variant="soft-rounded"
|
</HStack>
|
||||||
colorScheme="blue"
|
)}
|
||||||
size="lg"
|
</VStack>
|
||||||
index={currentTabIndex}
|
</HStack>
|
||||||
onChange={(index) => {
|
|
||||||
const tabNames = ['公司概览', '股票行情', '财务全景', '盈利预测'];
|
{/* 右侧:搜索和操作 */}
|
||||||
// 🎯 追踪 Tab 切换
|
<HStack spacing={3}>
|
||||||
trackTabChanged(index, tabNames[index], currentTabIndex);
|
{/* 搜索框 */}
|
||||||
setCurrentTabIndex(index);
|
<Box
|
||||||
|
sx={{
|
||||||
|
'.ant-select-selector': {
|
||||||
|
backgroundColor: `${THEME.bg} !important`,
|
||||||
|
borderColor: `${THEME.border} !important`,
|
||||||
|
borderRadius: '8px !important',
|
||||||
|
height: '40px !important',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: `${THEME.borderHover} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.ant-select-selection-search-input': {
|
||||||
|
color: `${THEME.textPrimary} !important`,
|
||||||
|
height: '38px !important',
|
||||||
|
},
|
||||||
|
'.ant-select-selection-placeholder': {
|
||||||
|
color: `${THEME.textMuted} !important`,
|
||||||
|
},
|
||||||
|
'.ant-select-dropdown': {
|
||||||
|
backgroundColor: `${THEME.cardBg} !important`,
|
||||||
|
borderColor: `${THEME.border} !important`,
|
||||||
|
},
|
||||||
|
'.ant-select-item': {
|
||||||
|
color: `${THEME.textPrimary} !important`,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: `${THEME.bg} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.ant-select-item-option-selected': {
|
||||||
|
backgroundColor: `rgba(212, 175, 55, 0.2) !important`,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabList p={4} bg={tabBg}>
|
<AutoComplete
|
||||||
<Tab
|
value={inputCode}
|
||||||
_selected={{
|
options={stockOptions}
|
||||||
bg: activeBg,
|
onSearch={doSearch}
|
||||||
color: 'white',
|
onSelect={handleStockSelect}
|
||||||
transform: 'scale(1.02)',
|
onChange={(value) => setInputCode(value)}
|
||||||
transition: 'all 0.2s'
|
placeholder="输入代码、名称或拼音"
|
||||||
}}
|
style={{ width: 220 }}
|
||||||
mr={2}
|
notFoundContent={isSearching ? <Spin size="small" /> : null}
|
||||||
>
|
onKeyDown={(e) => {
|
||||||
<HStack spacing={2}>
|
if (e.key === 'Enter') handleSearch();
|
||||||
<Icon as={FaInfoCircle} />
|
}}
|
||||||
<Text>公司概览</Text>
|
/>
|
||||||
</HStack>
|
</Box>
|
||||||
</Tab>
|
|
||||||
<Tab
|
{/* 搜索按钮 */}
|
||||||
_selected={{
|
<Button
|
||||||
bg: activeBg,
|
bg={THEME.gold}
|
||||||
color: 'white',
|
color={THEME.bg}
|
||||||
transform: 'scale(1.02)',
|
_hover={{ bg: THEME.goldLight }}
|
||||||
transition: 'all 0.2s'
|
_active={{ bg: THEME.goldDark }}
|
||||||
}}
|
size="md"
|
||||||
mr={2}
|
onClick={handleSearch}
|
||||||
>
|
leftIcon={<Icon as={Search} boxSize={4} />}
|
||||||
<HStack spacing={2}>
|
fontWeight="bold"
|
||||||
<Icon as={FaChartLine} />
|
>
|
||||||
<Text>股票行情</Text>
|
查询
|
||||||
</HStack>
|
</Button>
|
||||||
</Tab>
|
|
||||||
<Tab
|
{/* 自选按钮 */}
|
||||||
_selected={{
|
<Button
|
||||||
bg: activeBg,
|
variant={isInWatchlist ? 'solid' : 'outline'}
|
||||||
color: 'white',
|
bg={isInWatchlist ? THEME.gold : 'transparent'}
|
||||||
transform: 'scale(1.02)',
|
color={isInWatchlist ? THEME.bg : THEME.gold}
|
||||||
transition: 'all 0.2s'
|
borderColor={THEME.gold}
|
||||||
}}
|
_hover={{
|
||||||
mr={2}
|
bg: isInWatchlist ? THEME.goldLight : 'rgba(212, 175, 55, 0.1)',
|
||||||
>
|
}}
|
||||||
<HStack spacing={2}>
|
size="md"
|
||||||
<Icon as={FaMoneyBillWave} />
|
onClick={handleWatchlistToggle}
|
||||||
<Text>财务全景</Text>
|
isLoading={isWatchlistLoading}
|
||||||
</HStack>
|
leftIcon={<Icon as={Star} boxSize={4} fill={isInWatchlist ? 'currentColor' : 'none'} />}
|
||||||
</Tab>
|
fontWeight="bold"
|
||||||
<Tab
|
>
|
||||||
_selected={{
|
{isInWatchlist ? '已自选' : '自选'}
|
||||||
bg: activeBg,
|
</Button>
|
||||||
color: 'white',
|
</HStack>
|
||||||
transform: 'scale(1.02)',
|
</Flex>
|
||||||
transition: 'all 0.2s'
|
</Box>
|
||||||
}}
|
|
||||||
>
|
{/* 主内容区 - Tab 切换 */}
|
||||||
<HStack spacing={2}>
|
<Box maxW="container.xl" mx="auto" px={4} py={6}>
|
||||||
<Icon as={FaChartBar} />
|
<Box
|
||||||
<Text>盈利预测</Text>
|
bg={THEME.cardBg}
|
||||||
</HStack>
|
borderRadius="xl"
|
||||||
</Tab>
|
border="1px solid"
|
||||||
</TabList>
|
borderColor={THEME.border}
|
||||||
|
overflow="hidden"
|
||||||
<Divider />
|
>
|
||||||
|
<SubTabContainer
|
||||||
<TabPanels>
|
tabs={TAB_CONFIG}
|
||||||
<TabPanel p={6}>
|
componentProps={{ stockCode }}
|
||||||
<CompanyOverview stockCode={stockCode} />
|
onTabChange={handleTabChange}
|
||||||
</TabPanel>
|
themePreset="blackGold"
|
||||||
<TabPanel p={6}>
|
contentPadding={6}
|
||||||
<MarketDataView stockCode={stockCode} />
|
isLazy={true}
|
||||||
</TabPanel>
|
/>
|
||||||
<TabPanel p={6}>
|
</Box>
|
||||||
<FinancialPanorama stockCode={stockCode} />
|
</Box>
|
||||||
</TabPanel>
|
</Box>
|
||||||
<TabPanel p={6}>
|
|
||||||
<ForecastReport stockCode={stockCode} />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</VStack>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user