From 51721ce9bf5f4a34ca37ec27a6fadd0003fecc51 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 19 Dec 2025 10:14:07 +0800 Subject: [PATCH] =?UTF-8?q?perf(Company):=20=E4=BC=98=E5=8C=96=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E6=80=A7=E8=83=BD=E5=92=8C=20API=20=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StockQuoteCard: 添加 memo 包装减少重渲染 - Company/index: componentProps 使用 useMemo 缓存 - useCompanyEvents: 页面浏览事件只触发一次,避免重复追踪 - useCompanyData: 自选股状态改用单股票查询接口,减少数据传输 - CompanyHeader: inputCode 状态下移到 SearchActions,减少父组件重渲染 - CompanyHeader: 移除重复环境光效果,由全局 AmbientGlow 统一处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/CompanyHeader/index.tsx | 87 +++++++------------ .../components/StockQuoteCard/index.tsx | 4 +- src/views/Company/hooks/useCompanyData.ts | 34 ++++++-- src/views/Company/hooks/useCompanyEvents.js | 8 +- src/views/Company/index.tsx | 57 ++++++------ 5 files changed, 96 insertions(+), 94 deletions(-) diff --git a/src/views/Company/components/CompanyHeader/index.tsx b/src/views/Company/components/CompanyHeader/index.tsx index 8f25815b..73c0db1f 100644 --- a/src/views/Company/components/CompanyHeader/index.tsx +++ b/src/views/Company/components/CompanyHeader/index.tsx @@ -137,25 +137,29 @@ const StockInfoDisplay = memo<{ StockInfoDisplay.displayName = 'StockInfoDisplay'; /** - * 搜索操作区组件 + * 搜索操作区组件(状态自管理,减少父组件重渲染) */ const SearchActions = memo<{ - inputCode: string; - onInputChange: (value: string) => void; - onSearch: () => void; - onSelect: (value: string) => void; + stockCode: string; + onStockChange: (value: string) => void; isInWatchlist: boolean; watchlistLoading: boolean; onWatchlistToggle: () => void; }>(({ - inputCode, - onInputChange, - onSearch, - onSelect, + stockCode, + onStockChange, isInWatchlist, watchlistLoading, onWatchlistToggle, }) => { + // 输入状态自管理(避免父组件重渲染) + const [inputCode, setInputCode] = useState(stockCode); + + // 同步外部 stockCode 变化 + React.useEffect(() => { + setInputCode(stockCode); + }, [stockCode]); + // 股票搜索 Hook const searchHook = useStockSearch({ limit: 10, @@ -190,18 +194,28 @@ const SearchActions = memo<{ })); }, [searchResults]); + // 处理搜索按钮点击 + const handleSearch = useCallback(() => { + if (inputCode && inputCode !== stockCode) { + onStockChange(inputCode); + } + }, [inputCode, stockCode, onStockChange]); + // 选中股票 const handleSelect = useCallback((value: string) => { clearSearch(); - onSelect(value); - }, [clearSearch, onSelect]); + setInputCode(value); + if (value !== stockCode) { + onStockChange(value); + } + }, [clearSearch, stockCode, onStockChange]); // 键盘事件 const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter') { - onSearch(); + handleSearch(); } - }, [onSearch]); + }, [handleSearch]); return ( @@ -241,7 +255,7 @@ const SearchActions = memo<{ options={stockOptions} onSearch={doSearch} onSelect={handleSelect} - onChange={onInputChange} + onChange={setInputCode} placeholder="输入代码、名称或拼音" style={{ width: 240 }} dropdownStyle={{ @@ -271,7 +285,7 @@ const SearchActions = memo<{ size="md" h="42px" px={5} - onClick={onSearch} + onClick={handleSearch} leftIcon={} fontWeight="bold" borderRadius="10px" @@ -335,28 +349,6 @@ const CompanyHeader: React.FC = memo(({ onStockChange, onWatchlistToggle, }) => { - const [inputCode, setInputCode] = useState(stockCode); - - // 处理搜索 - const handleSearch = useCallback(() => { - if (inputCode && inputCode !== stockCode) { - onStockChange(inputCode); - } - }, [inputCode, stockCode, onStockChange]); - - // 处理选中 - const handleSelect = useCallback((value: string) => { - setInputCode(value); - if (value !== stockCode) { - onStockChange(value); - } - }, [stockCode, onStockChange]); - - // 同步 stockCode 变化 - React.useEffect(() => { - setInputCode(stockCode); - }, [stockCode]); - return ( = memo(({ backdropFilter={FUI_GLASS.blur.md} overflow="hidden" > - {/* 环境光效果 - James Turrell 风格 */} - - - {/* 顶部发光线 */} + {/* 顶部发光线(环境光效果由全局 AmbientGlow 提供) */} = memo(({ {/* 右侧:搜索和操作 */} = ({ ); }; -export default StockQuoteCard; +export default memo(StockQuoteCard); diff --git a/src/views/Company/hooks/useCompanyData.ts b/src/views/Company/hooks/useCompanyData.ts index c374d7b4..b507a7d6 100644 --- a/src/views/Company/hooks/useCompanyData.ts +++ b/src/views/Company/hooks/useCompanyData.ts @@ -76,26 +76,42 @@ export const useCompanyData = ({ }, [stockCode]); /** - * 加载自选股状态 + * 加载自选股状态(优化:只检查单个股票,避免加载整个列表) */ const loadWatchlistStatus = useCallback(async () => { - if (!isAuthenticated) { + if (!isAuthenticated || !stockCode) { setIsInWatchlist(false); return; } try { - const { data } = await axios.get>( - '/api/account/watchlist' + const { data } = await axios.get>( + `/api/account/watchlist/check/${stockCode}` ); - if (data.success && Array.isArray(data.data)) { - const codes = new Set(data.data.map((item) => item.stock_code)); - setIsInWatchlist(codes.has(stockCode)); + if (data.success && data.data) { + setIsInWatchlist(data.data.is_in_watchlist); + } else { + setIsInWatchlist(false); } } catch (error: any) { - logger.error('useCompanyData', 'loadWatchlistStatus', error); - setIsInWatchlist(false); + // 接口不存在时降级到原方案 + if (error.response?.status === 404) { + try { + const { data: listData } = await axios.get>( + '/api/account/watchlist' + ); + if (listData.success && Array.isArray(listData.data)) { + const codes = new Set(listData.data.map((item) => item.stock_code)); + setIsInWatchlist(codes.has(stockCode)); + } + } catch { + setIsInWatchlist(false); + } + } else { + logger.error('useCompanyData', 'loadWatchlistStatus', error); + setIsInWatchlist(false); + } } }, [stockCode, isAuthenticated]); diff --git a/src/views/Company/hooks/useCompanyEvents.js b/src/views/Company/hooks/useCompanyEvents.js index 5b5ed769..09853551 100644 --- a/src/views/Company/hooks/useCompanyEvents.js +++ b/src/views/Company/hooks/useCompanyEvents.js @@ -1,7 +1,7 @@ // src/views/Company/hooks/useCompanyEvents.js // 公司详情页面事件追踪 Hook -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { usePostHogTrack } from '../../../hooks/usePostHogRedux'; import { RETENTION_EVENTS } from '../../../lib/constants'; import { logger } from '../../../utils/logger'; @@ -14,9 +14,13 @@ import { logger } from '../../../utils/logger'; */ export const useCompanyEvents = ({ stockCode } = {}) => { const { track } = usePostHogTrack(); + const hasTrackedPageView = useRef(false); - // 🎯 页面浏览事件 - 页面加载时触发 + // 🎯 页面浏览事件 - 仅页面首次加载时触发一次 useEffect(() => { + if (hasTrackedPageView.current) return; + hasTrackedPageView.current = true; + track(RETENTION_EVENTS.COMPANY_PAGE_VIEWED, { timestamp: new Date().toISOString(), stock_code: stockCode || null, diff --git a/src/views/Company/index.tsx b/src/views/Company/index.tsx index 4697942c..3483414f 100644 --- a/src/views/Company/index.tsx +++ b/src/views/Company/index.tsx @@ -9,7 +9,7 @@ * - HeroUI 现代组件风格 */ -import React, { memo, useCallback, useRef, useEffect } from 'react'; +import React, { memo, useCallback, useRef, useEffect, useMemo } from 'react'; // FUI 动画样式 import './theme/fui-animations.css'; @@ -36,37 +36,42 @@ interface CompanyContentProps { onTabChange: (index: number, tabKey: string) => void; } -const CompanyContent = memo(({ +const CompanyContent: React.FC = memo(({ stockCode, isInWatchlist, watchlistLoading, onWatchlistToggle, onTabChange, -}) => ( - - {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */} - - - +}) => { + // 缓存 componentProps,避免每次渲染创建新对象 + const memoizedComponentProps = useMemo(() => ({ stockCode }), [stockCode]); - {/* Tab 内容区 - 使用 FuiContainer */} - - - - -)); + return ( + + {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */} + + + + + {/* Tab 内容区 - 使用 FuiContainer */} + + + + + ); +}); CompanyContent.displayName = 'CompanyContent';