fix(types): 修复 ECharts 类型导出和组件类型冲突

- echarts.ts: 将 EChartsOption 改为 EChartsCoreOption 的类型别名
- FuiCorners: 移除 extends BoxProps,position 重命名为 corner
- KLineChartModal/TimelineChartModal/ConcentrationCard: 使用导入的 EChartsOption
- LoadingState: 新增骨架屏 variant 支持
- FinancialPanorama: 使用骨架屏加载状态
- useFinancialData/financialService: 优化数据获取逻辑
- Company/index: 简化组件结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-18 18:42:19 +08:00
parent 5331bc64b4
commit 2720946ccf
10 changed files with 213 additions and 104 deletions

View File

@@ -19,8 +19,8 @@ export interface FuiCornersProps {
offset?: number; offset?: number;
} }
interface CornerBoxProps extends BoxProps { interface CornerBoxProps {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; corner: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
size: number; size: number;
borderWidth: number; borderWidth: number;
borderColor: string; borderColor: string;
@@ -29,14 +29,14 @@ interface CornerBoxProps extends BoxProps {
} }
const CornerBox = memo<CornerBoxProps>(({ const CornerBox = memo<CornerBoxProps>(({
position, corner,
size, size,
borderWidth, borderWidth,
borderColor, borderColor,
opacity, opacity,
offset, offset,
}) => { }) => {
const positionStyles: Record<string, BoxProps> = { const cornerStyles: Record<string, BoxProps> = {
'top-left': { 'top-left': {
top: `${offset}px`, top: `${offset}px`,
left: `${offset}px`, left: `${offset}px`,
@@ -71,7 +71,7 @@ const CornerBox = memo<CornerBoxProps>(({
borderColor={borderColor} borderColor={borderColor}
opacity={opacity} opacity={opacity}
pointerEvents="none" pointerEvents="none"
{...positionStyles[position]} {...cornerStyles[corner]}
/> />
); );
}); });
@@ -97,7 +97,7 @@ const FuiCorners = memo<FuiCornersProps>(({
opacity = 0.6, opacity = 0.6,
offset = 12, offset = 12,
}) => { }) => {
const positions: CornerBoxProps['position'][] = [ const corners: CornerBoxProps['corner'][] = [
'top-left', 'top-left',
'top-right', 'top-right',
'bottom-left', 'bottom-left',
@@ -106,10 +106,10 @@ const FuiCorners = memo<FuiCornersProps>(({
return ( return (
<> <>
{positions.map((position) => ( {corners.map((corner) => (
<CornerBox <CornerBox
key={position} key={corner}
position={position} corner={corner}
size={size} size={size}
borderWidth={borderWidth} borderWidth={borderWidth}
borderColor={borderColor} borderColor={borderColor}

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { echarts } from '@lib/echarts'; import { echarts } from '@lib/echarts';
import type { ECharts } from '@lib/echarts'; import type { ECharts, EChartsOption } from '@lib/echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { stockService } from '@services/eventService'; import { stockService } from '@services/eventService';
import { selectIsMobile } from '@store/slices/deviceSlice'; import { selectIsMobile } from '@store/slices/deviceSlice';
@@ -296,7 +296,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
} }
// 图表配置H5 响应式) // 图表配置H5 响应式)
const option: echarts.EChartsOption = { const option: EChartsOption = {
backgroundColor: '#1a1a1a', backgroundColor: '#1a1a1a',
title: { title: {
text: `${stock?.stock_name || stock?.stock_code} - 日K线`, text: `${stock?.stock_name || stock?.stock_code} - 日K线`,

View File

@@ -186,7 +186,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
} }
// 图表配置H5 响应式) // 图表配置H5 响应式)
const option: echarts.EChartsOption = { const option: EChartsOption = {
backgroundColor: '#1a1a1a', backgroundColor: '#1a1a1a',
title: { title: {
text: `${stock?.stock_name || stock?.stock_code} - 分时图`, text: `${stock?.stock_name || stock?.stock_code} - 分时图`,

View File

@@ -43,7 +43,7 @@ import { CanvasRenderer } from 'echarts/renderers';
// 类型导出 // 类型导出
import type { import type {
ECharts, ECharts,
EChartsOption, EChartsCoreOption,
SetOptionOpts, SetOptionOpts,
ComposeOption, ComposeOption,
} from 'echarts/core'; } from 'echarts/core';
@@ -114,7 +114,11 @@ export type ECOption = ComposeOption<
// 导出 // 导出
export { echarts }; export { echarts };
export type { ECharts, EChartsOption, SetOptionOpts };
// EChartsOption 类型别名(兼容旧代码)
export type EChartsOption = EChartsCoreOption;
export type { ECharts, SetOptionOpts };
// 默认导出(兼容 import * as echarts from 'echarts' 的用法) // 默认导出(兼容 import * as echarts from 'echarts' 的用法)
export default echarts; export default echarts;

View File

@@ -9,13 +9,17 @@ import axios from '@utils/axiosConfig';
/** /**
* 统一的 API 请求函数 * 统一的 API 请求函数
* axios 拦截器已自动处理日志记录 * axios 拦截器已自动处理日志记录
* @param {string} url - 请求 URL
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号,用于取消请求
*/ */
const apiRequest = async (url, options = {}) => { const apiRequest = async (url, options = {}) => {
const { method = 'GET', body, ...rest } = options; const { method = 'GET', body, signal, ...rest } = options;
const config = { const config = {
method, method,
url, url,
signal,
...rest, ...rest,
}; };
@@ -36,80 +40,98 @@ export const financialService = {
/** /**
* 获取股票基本信息和最新财务摘要 * 获取股票基本信息和最新财务摘要
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getStockInfo: async (seccode) => { getStockInfo: async (seccode, options = {}) => {
return await apiRequest(`/api/financial/stock-info/${seccode}`); return await apiRequest(`/api/financial/stock-info/${seccode}`, options);
}, },
/** /**
* 获取完整的资产负债表数据 * 获取完整的资产负债表数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量 * @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getBalanceSheet: async (seccode, limit = 12) => { getBalanceSheet: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`); return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`, options);
}, },
/** /**
* 获取完整的利润表数据 * 获取完整的利润表数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量 * @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getIncomeStatement: async (seccode, limit = 12) => { getIncomeStatement: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`); return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`, options);
}, },
/** /**
* 获取完整的现金流量表数据 * 获取完整的现金流量表数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量 * @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getCashflow: async (seccode, limit = 12) => { getCashflow: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`); return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`, options);
}, },
/** /**
* 获取完整的财务指标数据 * 获取完整的财务指标数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量 * @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getFinancialMetrics: async (seccode, limit = 12) => { getFinancialMetrics: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`); return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`, options);
}, },
/** /**
* 获取主营业务构成数据 * 获取主营业务构成数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} periods - 获取的报告期数量 * @param {number} periods - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getMainBusiness: async (seccode, periods = 4) => { getMainBusiness: async (seccode, periods = 4, options = {}) => {
return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`); return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`, options);
}, },
/** /**
* 获取业绩预告和预披露时间 * 获取业绩预告和预披露时间
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getForecast: async (seccode) => { getForecast: async (seccode, options = {}) => {
return await apiRequest(`/api/financial/forecast/${seccode}`); return await apiRequest(`/api/financial/forecast/${seccode}`, options);
}, },
/** /**
* 获取行业排名数据 * 获取行业排名数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量 * @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getIndustryRank: async (seccode, limit = 4) => { getIndustryRank: async (seccode, limit = 4, options = {}) => {
return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`); return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`, options);
}, },
/** /**
* 获取不同报告期的对比数据 * 获取不同报告期的对比数据
* @param {string} seccode - 股票代码 * @param {string} seccode - 股票代码
* @param {number} periods - 对比的报告期数量 * @param {number} periods - 对比的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/ */
getPeriodComparison: async (seccode, periods = 8) => { getPeriodComparison: async (seccode, periods = 8, options = {}) => {
return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`); return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`, options);
}, },
}; };

View File

@@ -99,7 +99,7 @@ const ConcentrationCard: React.FC<ConcentrationCardProps> = ({ concentration = [
chartInstance.current = echarts.init(chartRef.current); chartInstance.current = echarts.init(chartRef.current);
} }
const option: echarts.EChartsOption = { const option: EChartsOption = {
backgroundColor: "transparent", backgroundColor: "transparent",
tooltip: { tooltip: {
trigger: "item", trigger: "item",

View File

@@ -5,6 +5,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import axios from 'axios';
import { logger } from '@utils/logger'; import { logger } from '@utils/logger';
import { financialService } from '@services/financialService'; import { financialService } from '@services/financialService';
import type { import type {
@@ -19,6 +20,11 @@ import type {
ComparisonData, ComparisonData,
} from '../types'; } from '../types';
// 判断是否为取消请求的错误
const isCancelError = (error: unknown): boolean => {
return axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError');
};
// Tab key 到数据类型的映射 // Tab key 到数据类型的映射
export type DataTypeKey = export type DataTypeKey =
| 'balance' | 'balance'
@@ -102,6 +108,10 @@ export const useFinancialData = (
const isInitialLoad = useRef(true); const isInitialLoad = useRef(true);
const prevPeriods = useRef(selectedPeriods); const prevPeriods = useRef(selectedPeriods);
// AbortController refs - 用于取消请求
const coreDataControllerRef = useRef<AbortController | null>(null);
const tabDataControllerRef = useRef<AbortController | null>(null);
// 判断 Tab key 对应的数据类型 // 判断 Tab key 对应的数据类型
const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => { const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => {
switch (tabKey) { switch (tabKey) {
@@ -120,32 +130,36 @@ export const useFinancialData = (
// 按数据类型加载数据 // 按数据类型加载数据
const loadDataByType = useCallback(async ( const loadDataByType = useCallback(async (
dataType: 'balance' | 'income' | 'cashflow' | 'metrics', dataType: 'balance' | 'income' | 'cashflow' | 'metrics',
periods: number periods: number,
signal?: AbortSignal
) => { ) => {
const options: { signal?: AbortSignal } = signal ? { signal } : {};
try { try {
switch (dataType) { switch (dataType) {
case 'balance': { case 'balance': {
const res = await financialService.getBalanceSheet(stockCode, periods); const res = await financialService.getBalanceSheet(stockCode, periods, options);
if (res.success) setBalanceSheet(res.data); if (res.success) setBalanceSheet(res.data);
break; break;
} }
case 'income': { case 'income': {
const res = await financialService.getIncomeStatement(stockCode, periods); const res = await financialService.getIncomeStatement(stockCode, periods, options);
if (res.success) setIncomeStatement(res.data); if (res.success) setIncomeStatement(res.data);
break; break;
} }
case 'cashflow': { case 'cashflow': {
const res = await financialService.getCashflow(stockCode, periods); const res = await financialService.getCashflow(stockCode, periods, options);
if (res.success) setCashflow(res.data); if (res.success) setCashflow(res.data);
break; break;
} }
case 'metrics': { case 'metrics': {
const res = await financialService.getFinancialMetrics(stockCode, periods); const res = await financialService.getFinancialMetrics(stockCode, periods, options);
if (res.success) setFinancialMetrics(res.data); if (res.success) setFinancialMetrics(res.data);
break; break;
} }
} }
} catch (err) { } catch (err) {
// 取消请求不作为错误处理
if (isCancelError(err)) return;
logger.error('useFinancialData', 'loadDataByType', err, { dataType, periods }); logger.error('useFinancialData', 'loadDataByType', err, { dataType, periods });
throw err; throw err;
} }
@@ -157,6 +171,11 @@ export const useFinancialData = (
return; return;
} }
// 取消之前的 Tab 数据请求
tabDataControllerRef.current?.abort();
const controller = new AbortController();
tabDataControllerRef.current = controller;
const dataType = getDataTypeForTab(tabKey); const dataType = getDataTypeForTab(tabKey);
logger.debug('useFinancialData', '刷新单个 Tab 数据', { tabKey, dataType, selectedPeriods }); logger.debug('useFinancialData', '刷新单个 Tab 数据', { tabKey, dataType, selectedPeriods });
@@ -164,13 +183,18 @@ export const useFinancialData = (
setError(null); setError(null);
try { try {
await loadDataByType(dataType, selectedPeriods); await loadDataByType(dataType, selectedPeriods, controller.signal);
logger.info('useFinancialData', `${tabKey} 数据刷新成功`); logger.info('useFinancialData', `${tabKey} 数据刷新成功`);
} catch (err) { } catch (err) {
// 取消请求不作为错误处理
if (isCancelError(err)) return;
const errorMessage = err instanceof Error ? err.message : '未知错误'; const errorMessage = err instanceof Error ? err.message : '未知错误';
setError(errorMessage); setError(errorMessage);
} finally { } finally {
setLoadingTab(null); // 只有当前请求没有被取消时才设置 loading 状态
if (!controller.signal.aborted) {
setLoadingTab(null);
}
} }
}, [stockCode, selectedPeriods, loadDataByType]); }, [stockCode, selectedPeriods, loadDataByType]);
@@ -191,6 +215,12 @@ export const useFinancialData = (
return; return;
} }
// 取消之前的核心数据请求
coreDataControllerRef.current?.abort();
const controller = new AbortController();
coreDataControllerRef.current = controller;
const options = { signal: controller.signal };
logger.debug('useFinancialData', '开始加载核心财务数据', { stockCode, selectedPeriods }); logger.debug('useFinancialData', '开始加载核心财务数据', { stockCode, selectedPeriods });
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -203,10 +233,10 @@ export const useFinancialData = (
comparisonRes, comparisonRes,
businessRes, businessRes,
] = await Promise.all([ ] = await Promise.all([
financialService.getStockInfo(stockCode), financialService.getStockInfo(stockCode, options),
financialService.getFinancialMetrics(stockCode, selectedPeriods), financialService.getFinancialMetrics(stockCode, selectedPeriods, options),
financialService.getPeriodComparison(stockCode, selectedPeriods), financialService.getPeriodComparison(stockCode, selectedPeriods, options),
financialService.getMainBusiness(stockCode, 4), financialService.getMainBusiness(stockCode, 4, options),
]); ]);
// 设置数据 // 设置数据
@@ -217,11 +247,16 @@ export const useFinancialData = (
logger.info('useFinancialData', '核心财务数据加载成功', { stockCode }); logger.info('useFinancialData', '核心财务数据加载成功', { stockCode });
} catch (err) { } catch (err) {
// 取消请求不作为错误处理
if (isCancelError(err)) return;
const errorMessage = err instanceof Error ? err.message : '未知错误'; const errorMessage = err instanceof Error ? err.message : '未知错误';
setError(errorMessage); setError(errorMessage);
logger.error('useFinancialData', 'loadCoreFinancialData', err, { stockCode, selectedPeriods }); logger.error('useFinancialData', 'loadCoreFinancialData', err, { stockCode, selectedPeriods });
} finally { } finally {
setLoading(false); // 只有当前请求没有被取消时才设置 loading 状态
if (!controller.signal.aborted) {
setLoading(false);
}
} }
}, [stockCode, selectedPeriods, toast]); }, [stockCode, selectedPeriods, toast]);
@@ -253,6 +288,14 @@ export const useFinancialData = (
} }
}, [selectedPeriods, activeTab, refetchByTab]); }, [selectedPeriods, activeTab, refetchByTab]);
// 组件卸载时取消所有进行中的请求
useEffect(() => {
return () => {
coreDataControllerRef.current?.abort();
tabDataControllerRef.current?.abort();
};
}, []);
return { return {
// 数据状态 // 数据状态
stockInfo, stockInfo,

View File

@@ -283,7 +283,7 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
{/* 财务全景面板(三列布局:成长能力、盈利与回报、风险与运营) */} {/* 财务全景面板(三列布局:成长能力、盈利与回报、风险与运营) */}
{loading ? ( {loading ? (
<LoadingState message="加载财务数据中..." height="300px" /> <LoadingState message="加载财务数据中..." height="300px" variant="skeleton" skeletonRows={6} />
) : ( ) : (
<FinancialOverviewPanel <FinancialOverviewPanel
stockInfo={stockInfo} stockInfo={stockInfo}

View File

@@ -1,29 +1,97 @@
// src/views/Company/components/LoadingState.tsx // src/views/Company/components/LoadingState.tsx
// 统一的加载状态组件 - 黑金主题 // 统一的加载状态组件 - 黑金主题
import React from "react"; import React, { memo } from "react";
import { Center, VStack, Spinner, Text } from "@chakra-ui/react"; import { Center, VStack, Spinner, Text, Box, Skeleton, SimpleGrid } from "@chakra-ui/react";
// 黑金主题配置 // 黑金主题配置
const THEME = { const THEME = {
gold: "#D4AF37", gold: "#D4AF37",
goldLight: "rgba(212, 175, 55, 0.3)",
bgInset: "rgba(26, 32, 44, 0.6)",
borderGlass: "rgba(212, 175, 55, 0.2)",
textSecondary: "gray.400", textSecondary: "gray.400",
radiusSM: "md",
radiusMD: "lg",
}; };
interface LoadingStateProps { interface LoadingStateProps {
message?: string; message?: string;
height?: string; height?: string;
/** 使用骨架屏模式(更好的视觉体验) */
variant?: "spinner" | "skeleton";
/** 骨架屏行数 */
skeletonRows?: number;
} }
/**
* 骨架屏组件(黑金主题)
*/
const SkeletonContent: React.FC<{ rows: number }> = memo(({ rows }) => (
<VStack align="stretch" spacing={4} w="100%">
{/* 头部骨架 */}
<Box display="flex" justifyContent="space-between" alignItems="center">
<Skeleton
height="28px"
width="180px"
startColor={THEME.bgInset}
endColor={THEME.borderGlass}
borderRadius={THEME.radiusSM}
/>
<Skeleton
height="24px"
width="100px"
startColor={THEME.bgInset}
endColor={THEME.borderGlass}
borderRadius={THEME.radiusSM}
/>
</Box>
{/* 内容骨架行 */}
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
{Array.from({ length: Math.min(rows, 8) }).map((_, i) => (
<Skeleton
key={i}
height="60px"
startColor={THEME.bgInset}
endColor={THEME.borderGlass}
borderRadius={THEME.radiusMD}
/>
))}
</SimpleGrid>
{/* 图表区域骨架 */}
<Skeleton
height="200px"
startColor={THEME.bgInset}
endColor={THEME.borderGlass}
borderRadius={THEME.radiusMD}
/>
</VStack>
));
SkeletonContent.displayName = "SkeletonContent";
/** /**
* 统一的加载状态组件(黑金主题) * 统一的加载状态组件(黑金主题)
* *
* 用于所有一级 Tab 的 loading 状态展示 * 用于所有一级 Tab 的 loading 状态展示
* @param variant - "spinner"(默认)或 "skeleton"(骨架屏)
*/ */
const LoadingState: React.FC<LoadingStateProps> = ({ const LoadingState: React.FC<LoadingStateProps> = memo(({
message = "加载中...", message = "加载中...",
height = "300px", height = "300px",
variant = "spinner",
skeletonRows = 4,
}) => { }) => {
if (variant === "skeleton") {
return (
<Box h={height} p={4}>
<SkeletonContent rows={skeletonRows} />
</Box>
);
}
return ( return (
<Center h={height}> <Center h={height}>
<VStack spacing={4}> <VStack spacing={4}>
@@ -39,6 +107,8 @@ const LoadingState: React.FC<LoadingStateProps> = ({
</VStack> </VStack>
</Center> </Center>
); );
}; });
LoadingState.displayName = "LoadingState";
export default LoadingState; export default LoadingState;

View File

@@ -24,52 +24,6 @@ import CompanyHeader from './components/CompanyHeader';
import StockQuoteCard from './components/StockQuoteCard'; import StockQuoteCard from './components/StockQuoteCard';
import { THEME, TAB_CONFIG } from './config'; import { THEME, TAB_CONFIG } from './config';
// ============================================
// 主内容区组件 - FUI 风格
// ============================================
interface CompanyContentProps {
stockCode: string;
isInWatchlist: boolean;
watchlistLoading: boolean;
onWatchlistToggle: () => void;
onTabChange: (index: number, tabKey: string) => void;
}
const CompanyContent = memo<CompanyContentProps>(({
stockCode,
isInWatchlist,
watchlistLoading,
onWatchlistToggle,
onTabChange,
}) => (
<Box maxW="container.xl" mx="auto" px={4} py={6}>
{/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */}
<Box mb={6}>
<StockQuoteCard
stockCode={stockCode}
isInWatchlist={isInWatchlist}
isWatchlistLoading={watchlistLoading}
onWatchlistToggle={onWatchlistToggle}
/>
</Box>
{/* Tab 内容区 - 使用 FuiContainer */}
<FuiContainer variant="default">
<SubTabContainer
tabs={TAB_CONFIG}
componentProps={{ stockCode }}
onTabChange={onTabChange}
themePreset="blackGold"
contentPadding={0}
isLazy={true}
/>
</FuiContainer>
</Box>
));
CompanyContent.displayName = 'CompanyContent';
// ============================================ // ============================================
// 主页面组件 // 主页面组件
// ============================================ // ============================================
@@ -162,13 +116,29 @@ const CompanyIndex: React.FC = () => {
{/* 主内容区 */} {/* 主内容区 */}
<Box position="relative" zIndex={1}> <Box position="relative" zIndex={1}>
<CompanyContent <Box maxW="container.xl" mx="auto" px={4} py={6}>
stockCode={stockCode} {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */}
isInWatchlist={isInWatchlist} <Box mb={6}>
watchlistLoading={watchlistLoading} <StockQuoteCard
onWatchlistToggle={handleWatchlistToggle} stockCode={stockCode}
onTabChange={handleTabChange} isInWatchlist={isInWatchlist}
/> isWatchlistLoading={watchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
/>
</Box>
{/* Tab 内容区 - 使用 FuiContainer */}
<FuiContainer variant="default">
<SubTabContainer
tabs={TAB_CONFIG}
componentProps={{ stockCode }}
onTabChange={handleTabChange}
themePreset="blackGold"
contentPadding={0}
isLazy={true}
/>
</FuiContainer>
</Box>
</Box> </Box>
</Box> </Box>
); );