Merge branch 'feature_bugfix/251217_stock' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251217_stock

This commit is contained in:
2025-12-22 13:24:45 +08:00
14 changed files with 290 additions and 74 deletions

View File

@@ -55,41 +55,77 @@ export const generateMarketData = (stockCode) => {
repay: Math.floor(Math.random() * 500000000) + 80000000 // 融资偿还
},
securities: {
balance: Math.floor(Math.random() * 100000000) + 50000000, // 融券余额
balance: Math.floor(Math.random() * 100000000) + 50000000, // 融券余额(股数)
balance_amount: Math.floor(Math.random() * 2000000000) + 1000000000, // 融券余额(金额)
sell: Math.floor(Math.random() * 10000000) + 5000000, // 融券卖出
repay: Math.floor(Math.random() * 10000000) + 3000000 // 融券偿还
}
}))
},
// 大单统计 - 包含 daily_stats 数组
// 大宗交易 - 包含 daily_stats 数组,符合 BigDealDayStats 类型
bigDealData: {
success: true,
data: [],
daily_stats: Array(10).fill(null).map((_, i) => ({
date: new Date(Date.now() - (9 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
big_buy: Math.floor(Math.random() * 300000000) + 100000000,
big_sell: Math.floor(Math.random() * 300000000) + 80000000,
medium_buy: Math.floor(Math.random() * 200000000) + 60000000,
medium_sell: Math.floor(Math.random() * 200000000) + 50000000,
small_buy: Math.floor(Math.random() * 100000000) + 30000000,
small_sell: Math.floor(Math.random() * 100000000) + 25000000
}))
daily_stats: Array(10).fill(null).map((_, i) => {
const count = Math.floor(Math.random() * 5) + 1; // 1-5 笔交易
const avgPrice = parseFloat((basePrice * (0.95 + Math.random() * 0.1)).toFixed(2)); // 折价/溢价 -5%~+5%
const deals = Array(count).fill(null).map(() => {
const volume = parseFloat((Math.random() * 500 + 100).toFixed(2)); // 100-600 万股
const price = parseFloat((avgPrice * (0.98 + Math.random() * 0.04)).toFixed(2));
return {
buyer_dept: ['中信证券北京总部', '国泰君安上海分公司', '华泰证券深圳营业部', '招商证券广州分公司'][Math.floor(Math.random() * 4)],
seller_dept: ['中金公司北京营业部', '海通证券上海分公司', '广发证券深圳营业部', '平安证券广州分公司'][Math.floor(Math.random() * 4)],
price,
volume,
amount: parseFloat((price * volume).toFixed(2))
};
});
const totalVolume = deals.reduce((sum, d) => sum + d.volume, 0);
const totalAmount = deals.reduce((sum, d) => sum + d.amount, 0);
return {
date: new Date(Date.now() - (9 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
count,
total_volume: parseFloat(totalVolume.toFixed(2)),
total_amount: parseFloat(totalAmount.toFixed(2)),
avg_price: avgPrice,
deals
};
})
},
// 异动分析 - 包含 grouped_data 数组
// 龙虎榜数据 - 包含 grouped_data 数组,符合 UnusualDayData 类型
unusualData: {
success: true,
data: [],
grouped_data: Array(5).fill(null).map((_, i) => ({
date: new Date(Date.now() - (4 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
events: [
{ time: '14:35:22', type: '快速拉升', change: '+2.3%', description: '5分钟内上涨2.3%' },
{ time: '11:28:45', type: '大单买入', amount: '5680万', description: '单笔大单买入' },
{ time: '10:15:30', type: '量比异动', ratio: '3.2', description: '量比达到3.2倍' }
],
count: 3
}))
grouped_data: Array(5).fill(null).map((_, i) => {
const buyerDepts = ['中信证券北京总部', '国泰君安上海分公司', '华泰证券深圳营业部', '招商证券广州分公司', '中金公司北京营业部'];
const sellerDepts = ['海通证券上海分公司', '广发证券深圳营业部', '平安证券广州分公司', '东方证券上海营业部', '兴业证券福州营业部'];
const infoTypes = ['日涨幅偏离值达7%', '日振幅达15%', '连续三日涨幅偏离20%', '换手率达20%'];
const buyers = buyerDepts.map(dept => ({
dept_name: dept,
buy_amount: Math.floor(Math.random() * 50000000) + 10000000 // 1000万-6000万
})).sort((a, b) => b.buy_amount - a.buy_amount);
const sellers = sellerDepts.map(dept => ({
dept_name: dept,
sell_amount: Math.floor(Math.random() * 40000000) + 8000000 // 800万-4800万
})).sort((a, b) => b.sell_amount - a.sell_amount);
const totalBuy = buyers.reduce((sum, b) => sum + b.buy_amount, 0);
const totalSell = sellers.reduce((sum, s) => sum + s.sell_amount, 0);
return {
date: new Date(Date.now() - (4 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
total_buy: totalBuy,
total_sell: totalSell,
net_amount: totalBuy - totalSell,
buyers,
sellers,
info_types: infoTypes.slice(0, Math.floor(Math.random() * 3) + 1) // 随机选1-3个类型
};
})
},
// 股权质押 - 匹配 PledgeData[] 类型

View File

@@ -49,7 +49,7 @@ const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({ stockCode, isAc
};
if (loading) {
return <LoadingState message="加载公告数据..." />;
return <LoadingState variant="skeleton" skeletonType="list" skeletonCount={5} />;
}
return (

View File

@@ -27,7 +27,7 @@ const DisclosureSchedulePanel: React.FC<DisclosureSchedulePanelProps> = ({ stock
const { disclosureSchedule, loading } = useDisclosureData({ stockCode, enabled: isActive });
if (loading) {
return <LoadingState message="加载披露日程..." />;
return <LoadingState variant="skeleton" skeletonType="grid" skeletonCount={4} />;
}
if (disclosureSchedule.length === 0) {

View File

@@ -1,22 +1,110 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
// 复用的加载状态组件
// 复用的加载状态组件 - 支持骨架屏
import React from "react";
import { Center, VStack, Spinner, Text } from "@chakra-ui/react";
import React, { memo } from "react";
import {
Center,
VStack,
Spinner,
Text,
Box,
Skeleton,
SimpleGrid,
HStack,
} from "@chakra-ui/react";
import { THEME } from "../config";
// 骨架屏颜色配置
const SKELETON_COLORS = {
startColor: "rgba(26, 32, 44, 0.6)",
endColor: "rgba(212, 175, 55, 0.2)",
};
interface LoadingStateProps {
message?: string;
height?: string;
/** 使用骨架屏模式(更好的视觉体验) */
variant?: "spinner" | "skeleton";
/** 骨架屏类型grid网格布局或 list列表布局 */
skeletonType?: "grid" | "list";
/** 骨架屏项目数量 */
skeletonCount?: number;
}
/**
* 加载状态组件(黑金主题
* 网格骨架屏(用于披露日程等
*/
const LoadingState: React.FC<LoadingStateProps> = ({
const GridSkeleton: React.FC<{ count: number }> = memo(({ count }) => (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={3}>
{Array.from({ length: count }).map((_, i) => (
<Skeleton
key={i}
height="80px"
borderRadius="md"
{...SKELETON_COLORS}
/>
))}
</SimpleGrid>
));
GridSkeleton.displayName = "GridSkeleton";
/**
* 列表骨架屏(用于公告列表等)
*/
const ListSkeleton: React.FC<{ count: number }> = memo(({ count }) => (
<VStack spacing={2} align="stretch">
{Array.from({ length: count }).map((_, i) => (
<Box
key={i}
p={3}
borderRadius="md"
bg="rgba(26, 32, 44, 0.4)"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.2)"
>
<HStack justify="space-between">
<VStack align="start" spacing={2} flex={1}>
<HStack>
<Skeleton height="20px" width="60px" borderRadius="sm" {...SKELETON_COLORS} />
<Skeleton height="16px" width="80px" borderRadius="sm" {...SKELETON_COLORS} />
</HStack>
<Skeleton height="18px" width="90%" borderRadius="sm" {...SKELETON_COLORS} />
</VStack>
<Skeleton height="32px" width="32px" borderRadius="md" {...SKELETON_COLORS} />
</HStack>
</Box>
))}
</VStack>
));
ListSkeleton.displayName = "ListSkeleton";
/**
* 加载状态组件(黑金主题)
*
* @param variant - "spinner"(默认)或 "skeleton"(骨架屏)
* @param skeletonType - 骨架屏类型:"grid" 或 "list"
*/
const LoadingState: React.FC<LoadingStateProps> = memo(({
message = "加载中...",
height = "200px",
variant = "spinner",
skeletonType = "list",
skeletonCount = 4,
}) => {
if (variant === "skeleton") {
return (
<Box minH={height} p={4}>
{skeletonType === "grid" ? (
<GridSkeleton count={skeletonCount} />
) : (
<ListSkeleton count={skeletonCount} />
)}
</Box>
);
}
return (
<Center h={height}>
<VStack>
@@ -27,6 +115,8 @@ const LoadingState: React.FC<LoadingStateProps> = ({
</VStack>
</Center>
);
};
});
LoadingState.displayName = "LoadingState";
export default LoadingState;

View File

@@ -77,7 +77,7 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
themePreset="blackGold"
size="sm"
/>
<LoadingState message="加载数据中..." height="200px" />
<LoadingState variant="skeleton" height="300px" skeletonRows={6} />
</CardBody>
</Card>
);

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
// 公告数据 Hook - 用于公司公告 Tab
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig";
import type { Announcement } from "../types";
@@ -39,7 +39,11 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCode 和 refreshKey
const lastStockCodeRef = useRef<string | undefined>(undefined);
const lastRefreshKeyRef = useRef<number | undefined>(undefined);
useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求
@@ -48,6 +52,26 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
return;
}
// stockCode 或 refreshKey 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode || lastRefreshKeyRef.current !== refreshKey) {
// refreshKey 变化时强制重新加载
if (lastRefreshKeyRef.current !== refreshKey && lastRefreshKeyRef.current !== undefined) {
hasLoadedRef.current = false;
}
// stockCode 变化时重置
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
}
lastStockCodeRef.current = stockCode;
lastRefreshKeyRef.current = refreshKey;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController();
const loadData = async () => {
@@ -66,7 +90,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
setError("加载公告数据失败");
}
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
} catch (err: any) {
// 请求被取消时,不更新任何状态
if (err.name === "CanceledError") {
@@ -75,7 +99,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
setError("网络请求失败");
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
}
};
@@ -83,7 +107,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
return () => controller.abort();
}, [stockCode, enabled, refreshKey]);
const isLoading = loading || (enabled && !hasLoaded && !error);
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { announcements, loading: isLoading, error };
};

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
// 分支机构数据 Hook - 用于分支机构 Tab
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig";
import type { Branch } from "../types";
@@ -36,7 +36,10 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
const [branches, setBranches] = useState<Branch[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!enabled || !stockCode) {
@@ -44,6 +47,18 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
return;
}
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController();
const loadData = async () => {
@@ -62,7 +77,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
setError("加载分支机构数据失败");
}
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
} catch (err: any) {
// 请求被取消时,不更新任何状态
if (err.name === "CanceledError") {
@@ -71,7 +86,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
logger.error("useBranchesData", "loadData", err, { stockCode });
setError("网络请求失败");
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
}
};
@@ -79,7 +94,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
return () => controller.abort();
}, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error);
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { branches, loading: isLoading, error };
};

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
// 披露日程数据 Hook - 用于工商信息 Tab
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig";
import type { DisclosureSchedule } from "../types";
@@ -36,7 +36,10 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求
@@ -45,6 +48,18 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
return;
}
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController();
const loadData = async () => {
@@ -63,7 +78,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
setError("加载披露日程数据失败");
}
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
} catch (err: any) {
// 请求被取消时,不更新任何状态
if (err.name === "CanceledError") {
@@ -72,7 +87,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
logger.error("useDisclosureData", "loadData", err, { stockCode });
setError("网络请求失败");
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
}
};
@@ -80,7 +95,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
return () => controller.abort();
}, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error);
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { disclosureSchedule, loading: isLoading, error };
};

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
// 管理团队数据 Hook - 用于管理团队 Tab
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig";
import type { Management } from "../types";
@@ -36,7 +36,10 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
const [management, setManagement] = useState<Management[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求
@@ -45,6 +48,18 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
return;
}
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController();
const loadData = async () => {
@@ -63,7 +78,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
setError("加载管理团队数据失败");
}
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
} catch (err: any) {
// 请求被取消时,不更新任何状态
if (err.name === "CanceledError") {
@@ -72,7 +87,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
logger.error("useManagementData", "loadData", err, { stockCode });
setError("网络请求失败");
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
}
};
@@ -82,7 +97,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
// 派生 loading 状态enabled 但尚未完成首次加载时,视为 loading
// 这样可以在渲染时同步判断,避免 useEffect 异步导致的空状态闪烁
const isLoading = loading || (enabled && !hasLoaded && !error);
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return { management, loading: isLoading, error };
};

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
// 股权结构数据 Hook - 用于股权结构 Tab
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { logger } from "@utils/logger";
import axios from "@utils/axiosConfig";
import type { ActualControl, Concentration, Shareholder } from "../types";
@@ -42,7 +42,10 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
// 使用 ref 跟踪是否已加载,避免 Tab 切换时重复请求
const hasLoadedRef = useRef(false);
// 记录上次加载的 stockCodestockCode 变化时需要重新加载
const lastStockCodeRef = useRef<string | undefined>(undefined);
useEffect(() => {
// 只有 enabled 且有 stockCode 时才请求
@@ -51,6 +54,18 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
return;
}
// stockCode 变化时重置加载状态
if (lastStockCodeRef.current !== stockCode) {
hasLoadedRef.current = false;
lastStockCodeRef.current = stockCode;
}
// 如果已经加载过数据不再重新请求Tab 切换回来时保持缓存)
if (hasLoadedRef.current) {
setLoading(false);
return;
}
const controller = new AbortController();
const loadData = async () => {
@@ -75,7 +90,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
} catch (err: any) {
// 请求被取消时,不更新任何状态
if (err.name === "CanceledError") {
@@ -84,7 +99,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
logger.error("useShareholderData", "loadData", err, { stockCode });
setError("加载股权结构数据失败");
setLoading(false);
setHasLoaded(true);
hasLoadedRef.current = true;
}
};
@@ -92,7 +107,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
return () => controller.abort();
}, [stockCode, enabled]);
const isLoading = loading || (enabled && !hasLoaded && !error);
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
return {
actualControl,

View File

@@ -136,5 +136,5 @@ const MarketDataSkeleton: React.FC = memo(() => (
MarketDataSkeleton.displayName = 'MarketDataSkeleton';
export { MarketDataSkeleton };
export { MarketDataSkeleton, SummaryCardSkeleton };
export default MarketDataSkeleton;

View File

@@ -5,4 +5,4 @@ export { default as ThemedCard } from './ThemedCard';
export { default as MarkdownRenderer } from './MarkdownRenderer';
export { default as StockSummaryCard } from './StockSummaryCard';
export { default as AnalysisModal, AnalysisContent } from './AnalysisModal';
export { MarketDataSkeleton } from './MarketDataSkeleton';
export { MarketDataSkeleton, SummaryCardSkeleton } from './MarketDataSkeleton';

View File

@@ -33,8 +33,9 @@ export const useMarketData = (
period: number = DEFAULT_PERIOD
): UseMarketDataReturn => {
// 主数据状态
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [tradeLoading, setTradeLoading] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const [summary, setSummary] = useState<MarketSummary | null>(null);
const [tradeData, setTradeData] = useState<TradeDayData[]>([]);
const [fundingData, setFundingData] = useState<FundingDayData[]>([]);
@@ -153,15 +154,17 @@ export const useMarketData = (
if (loadedTradeData.length > 0) {
loadRiseAnalysis(loadedTradeData);
}
setLoading(false);
setHasLoaded(true);
} catch (error) {
// 取消请求不作为错误处理
if (isCancelError(error)) return;
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
} finally {
// 只有当前请求没有被取消时才设置 loading 状态
if (!controller.signal.aborted) {
setLoading(false);
// 请求被取消时,不更新任何状态
if (isCancelError(error)) {
return;
}
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
setLoading(false);
setHasLoaded(true);
}
}, [stockCode, period, loadRiseAnalysis]);
@@ -363,8 +366,11 @@ export const useMarketData = (
};
}, []);
// 派生 loading 状态stockCode 存在但尚未完成首次加载时,视为 loading
const isLoading = loading || (!!stockCode && !hasLoaded);
return {
loading,
loading: isLoading,
tradeLoading,
summary,
tradeData,

View File

@@ -22,6 +22,7 @@ import { useMarketData } from './hooks/useMarketData';
import {
ThemedCard,
StockSummaryCard,
SummaryCardSkeleton,
AnalysisModal,
AnalysisContent,
} from './components';
@@ -89,13 +90,12 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
}
}, [propStockCode, stockCode]);
// 首次渲染时加载默认 Tab融资融券的数据
// 首次挂载时加载默认 Tab融资融券的数据
// 注意SubTabContainer 的 onChange 只在切换时触发,首次渲染不会触发
useEffect(() => {
// 默认 Tab 是融资融券index 0
if (activeTab === 0) {
loadDataByType('funding');
}
}, [loadDataByType, activeTab]);
loadDataByType('funding');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 只在首次挂载时执行
// 处理图表点击事件
const handleChartClick = useCallback(
@@ -137,8 +137,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
<Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}>
<Container maxW="container.xl" py={4}>
<VStack align="stretch" spacing={4}>
{/* 股票概览 */}
{summary && <StockSummaryCard summary={summary} theme={theme} />}
{/* 股票概览 - 未加载时显示骨架屏占位,避免布局跳动 */}
{summary ? <StockSummaryCard summary={summary} theme={theme} /> : <SummaryCardSkeleton />}
{/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */}
<TradeDataPanel