From 2720946ccfc9585225d4aebf0858278a800a9f29 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 18 Dec 2025 18:42:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(types):=20=E4=BF=AE=E5=A4=8D=20ECharts=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=87=BA=E5=92=8C=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/FUI/FuiCorners.tsx | 18 ++--- src/components/StockChart/KLineChartModal.tsx | 4 +- .../StockChart/TimelineChartModal.tsx | 2 +- src/lib/echarts.ts | 8 +- src/services/financialService.js | 60 +++++++++----- .../shareholder/ConcentrationCard.tsx | 2 +- .../hooks/useFinancialData.ts | 67 +++++++++++++--- .../components/FinancialPanorama/index.tsx | 2 +- src/views/Company/components/LoadingState.tsx | 78 ++++++++++++++++++- src/views/Company/index.tsx | 76 ++++++------------ 10 files changed, 213 insertions(+), 104 deletions(-) diff --git a/src/components/FUI/FuiCorners.tsx b/src/components/FUI/FuiCorners.tsx index 632d0fda..8768d135 100644 --- a/src/components/FUI/FuiCorners.tsx +++ b/src/components/FUI/FuiCorners.tsx @@ -19,8 +19,8 @@ export interface FuiCornersProps { offset?: number; } -interface CornerBoxProps extends BoxProps { - position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +interface CornerBoxProps { + corner: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; size: number; borderWidth: number; borderColor: string; @@ -29,14 +29,14 @@ interface CornerBoxProps extends BoxProps { } const CornerBox = memo(({ - position, + corner, size, borderWidth, borderColor, opacity, offset, }) => { - const positionStyles: Record = { + const cornerStyles: Record = { 'top-left': { top: `${offset}px`, left: `${offset}px`, @@ -71,7 +71,7 @@ const CornerBox = memo(({ borderColor={borderColor} opacity={opacity} pointerEvents="none" - {...positionStyles[position]} + {...cornerStyles[corner]} /> ); }); @@ -97,7 +97,7 @@ const FuiCorners = memo(({ opacity = 0.6, offset = 12, }) => { - const positions: CornerBoxProps['position'][] = [ + const corners: CornerBoxProps['corner'][] = [ 'top-left', 'top-right', 'bottom-left', @@ -106,10 +106,10 @@ const FuiCorners = memo(({ return ( <> - {positions.map((position) => ( + {corners.map((corner) => ( = ({ } // 图表配置(H5 响应式) - const option: echarts.EChartsOption = { + const option: EChartsOption = { backgroundColor: '#1a1a1a', title: { text: `${stock?.stock_name || stock?.stock_code} - 日K线`, diff --git a/src/components/StockChart/TimelineChartModal.tsx b/src/components/StockChart/TimelineChartModal.tsx index b55bd717..3406084f 100644 --- a/src/components/StockChart/TimelineChartModal.tsx +++ b/src/components/StockChart/TimelineChartModal.tsx @@ -186,7 +186,7 @@ const TimelineChartModal: React.FC = ({ } // 图表配置(H5 响应式) - const option: echarts.EChartsOption = { + const option: EChartsOption = { backgroundColor: '#1a1a1a', title: { text: `${stock?.stock_name || stock?.stock_code} - 分时图`, diff --git a/src/lib/echarts.ts b/src/lib/echarts.ts index 0bd967d1..b1aa5009 100644 --- a/src/lib/echarts.ts +++ b/src/lib/echarts.ts @@ -43,7 +43,7 @@ import { CanvasRenderer } from 'echarts/renderers'; // 类型导出 import type { ECharts, - EChartsOption, + EChartsCoreOption, SetOptionOpts, ComposeOption, } from 'echarts/core'; @@ -114,7 +114,11 @@ export type ECOption = ComposeOption< // 导出 export { echarts }; -export type { ECharts, EChartsOption, SetOptionOpts }; + +// EChartsOption 类型别名(兼容旧代码) +export type EChartsOption = EChartsCoreOption; + +export type { ECharts, SetOptionOpts }; // 默认导出(兼容 import * as echarts from 'echarts' 的用法) export default echarts; diff --git a/src/services/financialService.js b/src/services/financialService.js index babc27b3..ba04cdb2 100644 --- a/src/services/financialService.js +++ b/src/services/financialService.js @@ -9,13 +9,17 @@ import axios from '@utils/axiosConfig'; /** * 统一的 API 请求函数 * axios 拦截器已自动处理日志记录 + * @param {string} url - 请求 URL + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号,用于取消请求 */ const apiRequest = async (url, options = {}) => { - const { method = 'GET', body, ...rest } = options; + const { method = 'GET', body, signal, ...rest } = options; const config = { method, url, + signal, ...rest, }; @@ -36,80 +40,98 @@ export const financialService = { /** * 获取股票基本信息和最新财务摘要 * @param {string} seccode - 股票代码 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getStockInfo: async (seccode) => { - return await apiRequest(`/api/financial/stock-info/${seccode}`); + getStockInfo: async (seccode, options = {}) => { + return await apiRequest(`/api/financial/stock-info/${seccode}`, options); }, /** * 获取完整的资产负债表数据 * @param {string} seccode - 股票代码 * @param {number} limit - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getBalanceSheet: async (seccode, limit = 12) => { - return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`); + getBalanceSheet: async (seccode, limit = 12, options = {}) => { + return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`, options); }, /** * 获取完整的利润表数据 * @param {string} seccode - 股票代码 * @param {number} limit - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getIncomeStatement: async (seccode, limit = 12) => { - return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`); + getIncomeStatement: async (seccode, limit = 12, options = {}) => { + return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`, options); }, /** * 获取完整的现金流量表数据 * @param {string} seccode - 股票代码 * @param {number} limit - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getCashflow: async (seccode, limit = 12) => { - return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`); + getCashflow: async (seccode, limit = 12, options = {}) => { + return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`, options); }, /** * 获取完整的财务指标数据 * @param {string} seccode - 股票代码 * @param {number} limit - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getFinancialMetrics: async (seccode, limit = 12) => { - return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`); + getFinancialMetrics: async (seccode, limit = 12, options = {}) => { + return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`, options); }, /** * 获取主营业务构成数据 * @param {string} seccode - 股票代码 * @param {number} periods - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getMainBusiness: async (seccode, periods = 4) => { - return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`); + getMainBusiness: async (seccode, periods = 4, options = {}) => { + return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`, options); }, /** * 获取业绩预告和预披露时间 * @param {string} seccode - 股票代码 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getForecast: async (seccode) => { - return await apiRequest(`/api/financial/forecast/${seccode}`); + getForecast: async (seccode, options = {}) => { + return await apiRequest(`/api/financial/forecast/${seccode}`, options); }, /** * 获取行业排名数据 * @param {string} seccode - 股票代码 * @param {number} limit - 获取的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getIndustryRank: async (seccode, limit = 4) => { - return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`); + getIndustryRank: async (seccode, limit = 4, options = {}) => { + return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`, options); }, /** * 获取不同报告期的对比数据 * @param {string} seccode - 股票代码 * @param {number} periods - 对比的报告期数量 + * @param {object} options - 请求选项 + * @param {AbortSignal} options.signal - AbortController 信号 */ - getPeriodComparison: async (seccode, periods = 8) => { - return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`); + getPeriodComparison: async (seccode, periods = 8, options = {}) => { + return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`, options); }, }; diff --git a/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx b/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx index d3fa7102..bb600010 100644 --- a/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx +++ b/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx @@ -99,7 +99,7 @@ const ConcentrationCard: React.FC = ({ concentration = [ chartInstance.current = echarts.init(chartRef.current); } - const option: echarts.EChartsOption = { + const option: EChartsOption = { backgroundColor: "transparent", tooltip: { trigger: "item", diff --git a/src/views/Company/components/FinancialPanorama/hooks/useFinancialData.ts b/src/views/Company/components/FinancialPanorama/hooks/useFinancialData.ts index 68cf886c..130bb313 100644 --- a/src/views/Company/components/FinancialPanorama/hooks/useFinancialData.ts +++ b/src/views/Company/components/FinancialPanorama/hooks/useFinancialData.ts @@ -5,6 +5,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useToast } from '@chakra-ui/react'; +import axios from 'axios'; import { logger } from '@utils/logger'; import { financialService } from '@services/financialService'; import type { @@ -19,6 +20,11 @@ import type { ComparisonData, } from '../types'; +// 判断是否为取消请求的错误 +const isCancelError = (error: unknown): boolean => { + return axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError'); +}; + // Tab key 到数据类型的映射 export type DataTypeKey = | 'balance' @@ -102,6 +108,10 @@ export const useFinancialData = ( const isInitialLoad = useRef(true); const prevPeriods = useRef(selectedPeriods); + // AbortController refs - 用于取消请求 + const coreDataControllerRef = useRef(null); + const tabDataControllerRef = useRef(null); + // 判断 Tab key 对应的数据类型 const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => { switch (tabKey) { @@ -120,32 +130,36 @@ export const useFinancialData = ( // 按数据类型加载数据 const loadDataByType = useCallback(async ( dataType: 'balance' | 'income' | 'cashflow' | 'metrics', - periods: number + periods: number, + signal?: AbortSignal ) => { + const options: { signal?: AbortSignal } = signal ? { signal } : {}; try { switch (dataType) { case 'balance': { - const res = await financialService.getBalanceSheet(stockCode, periods); + const res = await financialService.getBalanceSheet(stockCode, periods, options); if (res.success) setBalanceSheet(res.data); break; } case 'income': { - const res = await financialService.getIncomeStatement(stockCode, periods); + const res = await financialService.getIncomeStatement(stockCode, periods, options); if (res.success) setIncomeStatement(res.data); break; } case 'cashflow': { - const res = await financialService.getCashflow(stockCode, periods); + const res = await financialService.getCashflow(stockCode, periods, options); if (res.success) setCashflow(res.data); break; } case 'metrics': { - const res = await financialService.getFinancialMetrics(stockCode, periods); + const res = await financialService.getFinancialMetrics(stockCode, periods, options); if (res.success) setFinancialMetrics(res.data); break; } } } catch (err) { + // 取消请求不作为错误处理 + if (isCancelError(err)) return; logger.error('useFinancialData', 'loadDataByType', err, { dataType, periods }); throw err; } @@ -157,6 +171,11 @@ export const useFinancialData = ( return; } + // 取消之前的 Tab 数据请求 + tabDataControllerRef.current?.abort(); + const controller = new AbortController(); + tabDataControllerRef.current = controller; + const dataType = getDataTypeForTab(tabKey); logger.debug('useFinancialData', '刷新单个 Tab 数据', { tabKey, dataType, selectedPeriods }); @@ -164,13 +183,18 @@ export const useFinancialData = ( setError(null); try { - await loadDataByType(dataType, selectedPeriods); + await loadDataByType(dataType, selectedPeriods, controller.signal); logger.info('useFinancialData', `${tabKey} 数据刷新成功`); } catch (err) { + // 取消请求不作为错误处理 + if (isCancelError(err)) return; const errorMessage = err instanceof Error ? err.message : '未知错误'; setError(errorMessage); } finally { - setLoadingTab(null); + // 只有当前请求没有被取消时才设置 loading 状态 + if (!controller.signal.aborted) { + setLoadingTab(null); + } } }, [stockCode, selectedPeriods, loadDataByType]); @@ -191,6 +215,12 @@ export const useFinancialData = ( return; } + // 取消之前的核心数据请求 + coreDataControllerRef.current?.abort(); + const controller = new AbortController(); + coreDataControllerRef.current = controller; + const options = { signal: controller.signal }; + logger.debug('useFinancialData', '开始加载核心财务数据', { stockCode, selectedPeriods }); setLoading(true); setError(null); @@ -203,10 +233,10 @@ export const useFinancialData = ( comparisonRes, businessRes, ] = await Promise.all([ - financialService.getStockInfo(stockCode), - financialService.getFinancialMetrics(stockCode, selectedPeriods), - financialService.getPeriodComparison(stockCode, selectedPeriods), - financialService.getMainBusiness(stockCode, 4), + financialService.getStockInfo(stockCode, options), + financialService.getFinancialMetrics(stockCode, selectedPeriods, options), + financialService.getPeriodComparison(stockCode, selectedPeriods, options), + financialService.getMainBusiness(stockCode, 4, options), ]); // 设置数据 @@ -217,11 +247,16 @@ export const useFinancialData = ( logger.info('useFinancialData', '核心财务数据加载成功', { stockCode }); } catch (err) { + // 取消请求不作为错误处理 + if (isCancelError(err)) return; const errorMessage = err instanceof Error ? err.message : '未知错误'; setError(errorMessage); logger.error('useFinancialData', 'loadCoreFinancialData', err, { stockCode, selectedPeriods }); } finally { - setLoading(false); + // 只有当前请求没有被取消时才设置 loading 状态 + if (!controller.signal.aborted) { + setLoading(false); + } } }, [stockCode, selectedPeriods, toast]); @@ -253,6 +288,14 @@ export const useFinancialData = ( } }, [selectedPeriods, activeTab, refetchByTab]); + // 组件卸载时取消所有进行中的请求 + useEffect(() => { + return () => { + coreDataControllerRef.current?.abort(); + tabDataControllerRef.current?.abort(); + }; + }, []); + return { // 数据状态 stockInfo, diff --git a/src/views/Company/components/FinancialPanorama/index.tsx b/src/views/Company/components/FinancialPanorama/index.tsx index d68dd2d8..7fdcf6a4 100644 --- a/src/views/Company/components/FinancialPanorama/index.tsx +++ b/src/views/Company/components/FinancialPanorama/index.tsx @@ -283,7 +283,7 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt {/* 财务全景面板(三列布局:成长能力、盈利与回报、风险与运营) */} {loading ? ( - + ) : ( = memo(({ rows }) => ( + + {/* 头部骨架 */} + + + + + + {/* 内容骨架行 */} + + {Array.from({ length: Math.min(rows, 8) }).map((_, i) => ( + + ))} + + + {/* 图表区域骨架 */} + + +)); + +SkeletonContent.displayName = "SkeletonContent"; + /** * 统一的加载状态组件(黑金主题) * * 用于所有一级 Tab 的 loading 状态展示 + * @param variant - "spinner"(默认)或 "skeleton"(骨架屏) */ -const LoadingState: React.FC = ({ +const LoadingState: React.FC = memo(({ message = "加载中...", height = "300px", + variant = "spinner", + skeletonRows = 4, }) => { + if (variant === "skeleton") { + return ( + + + + ); + } + return (
@@ -39,6 +107,8 @@ const LoadingState: React.FC = ({
); -}; +}); + +LoadingState.displayName = "LoadingState"; export default LoadingState; diff --git a/src/views/Company/index.tsx b/src/views/Company/index.tsx index 4697942c..a0565b09 100644 --- a/src/views/Company/index.tsx +++ b/src/views/Company/index.tsx @@ -24,52 +24,6 @@ import CompanyHeader from './components/CompanyHeader'; import StockQuoteCard from './components/StockQuoteCard'; 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(({ - stockCode, - isInWatchlist, - watchlistLoading, - onWatchlistToggle, - onTabChange, -}) => ( - - {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */} - - - - - {/* Tab 内容区 - 使用 FuiContainer */} - - - - -)); - -CompanyContent.displayName = 'CompanyContent'; - // ============================================ // 主页面组件 // ============================================ @@ -162,13 +116,29 @@ const CompanyIndex: React.FC = () => { {/* 主内容区 */} - + + {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */} + + + + + {/* Tab 内容区 - 使用 FuiContainer */} + + + + );