Merge branch 'feature_bugfix/251217_stock' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251217_stock
This commit is contained in:
@@ -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[] 类型
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
// 记录上次加载的 stockCode,stockCode 变化时需要重新加载
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
// 记录上次加载的 stockCode,stockCode 变化时需要重新加载
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
// 记录上次加载的 stockCode,stockCode 变化时需要重新加载
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
// 记录上次加载的 stockCode,stockCode 变化时需要重新加载
|
||||
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,
|
||||
|
||||
@@ -136,5 +136,5 @@ const MarketDataSkeleton: React.FC = memo(() => (
|
||||
|
||||
MarketDataSkeleton.displayName = 'MarketDataSkeleton';
|
||||
|
||||
export { MarketDataSkeleton };
|
||||
export { MarketDataSkeleton, SummaryCardSkeleton };
|
||||
export default MarketDataSkeleton;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user