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 // 融资偿还
|
repay: Math.floor(Math.random() * 500000000) + 80000000 // 融资偿还
|
||||||
},
|
},
|
||||||
securities: {
|
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, // 融券卖出
|
sell: Math.floor(Math.random() * 10000000) + 5000000, // 融券卖出
|
||||||
repay: Math.floor(Math.random() * 10000000) + 3000000 // 融券偿还
|
repay: Math.floor(Math.random() * 10000000) + 3000000 // 融券偿还
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
// 大单统计 - 包含 daily_stats 数组
|
// 大宗交易 - 包含 daily_stats 数组,符合 BigDealDayStats 类型
|
||||||
bigDealData: {
|
bigDealData: {
|
||||||
success: true,
|
success: true,
|
||||||
data: [],
|
data: [],
|
||||||
daily_stats: Array(10).fill(null).map((_, i) => ({
|
daily_stats: Array(10).fill(null).map((_, i) => {
|
||||||
date: new Date(Date.now() - (9 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
const count = Math.floor(Math.random() * 5) + 1; // 1-5 笔交易
|
||||||
big_buy: Math.floor(Math.random() * 300000000) + 100000000,
|
const avgPrice = parseFloat((basePrice * (0.95 + Math.random() * 0.1)).toFixed(2)); // 折价/溢价 -5%~+5%
|
||||||
big_sell: Math.floor(Math.random() * 300000000) + 80000000,
|
const deals = Array(count).fill(null).map(() => {
|
||||||
medium_buy: Math.floor(Math.random() * 200000000) + 60000000,
|
const volume = parseFloat((Math.random() * 500 + 100).toFixed(2)); // 100-600 万股
|
||||||
medium_sell: Math.floor(Math.random() * 200000000) + 50000000,
|
const price = parseFloat((avgPrice * (0.98 + Math.random() * 0.04)).toFixed(2));
|
||||||
small_buy: Math.floor(Math.random() * 100000000) + 30000000,
|
return {
|
||||||
small_sell: Math.floor(Math.random() * 100000000) + 25000000
|
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: {
|
unusualData: {
|
||||||
success: true,
|
success: true,
|
||||||
data: [],
|
data: [],
|
||||||
grouped_data: Array(5).fill(null).map((_, i) => ({
|
grouped_data: Array(5).fill(null).map((_, i) => {
|
||||||
date: new Date(Date.now() - (4 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
const buyerDepts = ['中信证券北京总部', '国泰君安上海分公司', '华泰证券深圳营业部', '招商证券广州分公司', '中金公司北京营业部'];
|
||||||
events: [
|
const sellerDepts = ['海通证券上海分公司', '广发证券深圳营业部', '平安证券广州分公司', '东方证券上海营业部', '兴业证券福州营业部'];
|
||||||
{ time: '14:35:22', type: '快速拉升', change: '+2.3%', description: '5分钟内上涨2.3%' },
|
const infoTypes = ['日涨幅偏离值达7%', '日振幅达15%', '连续三日涨幅偏离20%', '换手率达20%'];
|
||||||
{ time: '11:28:45', type: '大单买入', amount: '5680万', description: '单笔大单买入' },
|
|
||||||
{ time: '10:15:30', type: '量比异动', ratio: '3.2', description: '量比达到3.2倍' }
|
const buyers = buyerDepts.map(dept => ({
|
||||||
],
|
dept_name: dept,
|
||||||
count: 3
|
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[] 类型
|
// 股权质押 - 匹配 PledgeData[] 类型
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({ stockCode, isAc
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingState message="加载公告数据..." />;
|
return <LoadingState variant="skeleton" skeletonType="list" skeletonCount={5} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const DisclosureSchedulePanel: React.FC<DisclosureSchedulePanelProps> = ({ stock
|
|||||||
const { disclosureSchedule, loading } = useDisclosureData({ stockCode, enabled: isActive });
|
const { disclosureSchedule, loading } = useDisclosureData({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingState message="加载披露日程..." />;
|
return <LoadingState variant="skeleton" skeletonType="grid" skeletonCount={4} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disclosureSchedule.length === 0) {
|
if (disclosureSchedule.length === 0) {
|
||||||
|
|||||||
@@ -1,22 +1,110 @@
|
|||||||
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/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,
|
||||||
|
HStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { THEME } from "../config";
|
import { THEME } from "../config";
|
||||||
|
|
||||||
|
// 骨架屏颜色配置
|
||||||
|
const SKELETON_COLORS = {
|
||||||
|
startColor: "rgba(26, 32, 44, 0.6)",
|
||||||
|
endColor: "rgba(212, 175, 55, 0.2)",
|
||||||
|
};
|
||||||
|
|
||||||
interface LoadingStateProps {
|
interface LoadingStateProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
height?: 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 = "加载中...",
|
message = "加载中...",
|
||||||
height = "200px",
|
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 (
|
return (
|
||||||
<Center h={height}>
|
<Center h={height}>
|
||||||
<VStack>
|
<VStack>
|
||||||
@@ -27,6 +115,8 @@ const LoadingState: React.FC<LoadingStateProps> = ({
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
LoadingState.displayName = "LoadingState";
|
||||||
|
|
||||||
export default LoadingState;
|
export default LoadingState;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
|
|||||||
themePreset="blackGold"
|
themePreset="blackGold"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<LoadingState message="加载数据中..." height="200px" />
|
<LoadingState variant="skeleton" height="300px" skeletonRows={6} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
|
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
|
||||||
// 公告数据 Hook - 用于公司公告 Tab
|
// 公告数据 Hook - 用于公司公告 Tab
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { logger } from "@utils/logger";
|
import { logger } from "@utils/logger";
|
||||||
import axios from "@utils/axiosConfig";
|
import axios from "@utils/axiosConfig";
|
||||||
import type { Announcement } from "../types";
|
import type { Announcement } from "../types";
|
||||||
@@ -39,7 +39,11 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
|
|||||||
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
// 只有 enabled 且有 stockCode 时才请求
|
// 只有 enabled 且有 stockCode 时才请求
|
||||||
@@ -48,6 +52,26 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
|
|||||||
return;
|
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 controller = new AbortController();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -66,7 +90,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
|
|||||||
setError("加载公告数据失败");
|
setError("加载公告数据失败");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 请求被取消时,不更新任何状态
|
// 请求被取消时,不更新任何状态
|
||||||
if (err.name === "CanceledError") {
|
if (err.name === "CanceledError") {
|
||||||
@@ -75,7 +99,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
|
|||||||
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
||||||
setError("网络请求失败");
|
setError("网络请求失败");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +107,7 @@ export const useAnnouncementsData = (options: UseAnnouncementsDataOptions): UseA
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [stockCode, enabled, refreshKey]);
|
}, [stockCode, enabled, refreshKey]);
|
||||||
|
|
||||||
const isLoading = loading || (enabled && !hasLoaded && !error);
|
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
|
||||||
|
|
||||||
return { announcements, loading: isLoading, error };
|
return { announcements, loading: isLoading, error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
|
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
|
||||||
// 分支机构数据 Hook - 用于分支机构 Tab
|
// 分支机构数据 Hook - 用于分支机构 Tab
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { logger } from "@utils/logger";
|
import { logger } from "@utils/logger";
|
||||||
import axios from "@utils/axiosConfig";
|
import axios from "@utils/axiosConfig";
|
||||||
import type { Branch } from "../types";
|
import type { Branch } from "../types";
|
||||||
@@ -36,7 +36,10 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
|
|||||||
const [branches, setBranches] = useState<Branch[]>([]);
|
const [branches, setBranches] = useState<Branch[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!enabled || !stockCode) {
|
if (!enabled || !stockCode) {
|
||||||
@@ -44,6 +47,18 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stockCode 变化时重置加载状态
|
||||||
|
if (lastStockCodeRef.current !== stockCode) {
|
||||||
|
hasLoadedRef.current = false;
|
||||||
|
lastStockCodeRef.current = stockCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经加载过数据,不再重新请求(Tab 切换回来时保持缓存)
|
||||||
|
if (hasLoadedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -62,7 +77,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
|
|||||||
setError("加载分支机构数据失败");
|
setError("加载分支机构数据失败");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 请求被取消时,不更新任何状态
|
// 请求被取消时,不更新任何状态
|
||||||
if (err.name === "CanceledError") {
|
if (err.name === "CanceledError") {
|
||||||
@@ -71,7 +86,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
|
|||||||
logger.error("useBranchesData", "loadData", err, { stockCode });
|
logger.error("useBranchesData", "loadData", err, { stockCode });
|
||||||
setError("网络请求失败");
|
setError("网络请求失败");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +94,7 @@ export const useBranchesData = (options: UseBranchesDataOptions): UseBranchesDat
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [stockCode, enabled]);
|
}, [stockCode, enabled]);
|
||||||
|
|
||||||
const isLoading = loading || (enabled && !hasLoaded && !error);
|
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
|
||||||
|
|
||||||
return { branches, loading: isLoading, error };
|
return { branches, loading: isLoading, error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
|
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
|
||||||
// 披露日程数据 Hook - 用于工商信息 Tab
|
// 披露日程数据 Hook - 用于工商信息 Tab
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { logger } from "@utils/logger";
|
import { logger } from "@utils/logger";
|
||||||
import axios from "@utils/axiosConfig";
|
import axios from "@utils/axiosConfig";
|
||||||
import type { DisclosureSchedule } from "../types";
|
import type { DisclosureSchedule } from "../types";
|
||||||
@@ -36,7 +36,10 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
|
|||||||
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
|
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
// 只有 enabled 且有 stockCode 时才请求
|
// 只有 enabled 且有 stockCode 时才请求
|
||||||
@@ -45,6 +48,18 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stockCode 变化时重置加载状态
|
||||||
|
if (lastStockCodeRef.current !== stockCode) {
|
||||||
|
hasLoadedRef.current = false;
|
||||||
|
lastStockCodeRef.current = stockCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经加载过数据,不再重新请求(Tab 切换回来时保持缓存)
|
||||||
|
if (hasLoadedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -63,7 +78,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
|
|||||||
setError("加载披露日程数据失败");
|
setError("加载披露日程数据失败");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 请求被取消时,不更新任何状态
|
// 请求被取消时,不更新任何状态
|
||||||
if (err.name === "CanceledError") {
|
if (err.name === "CanceledError") {
|
||||||
@@ -72,7 +87,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
|
|||||||
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
||||||
setError("网络请求失败");
|
setError("网络请求失败");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +95,7 @@ export const useDisclosureData = (options: UseDisclosureDataOptions): UseDisclos
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [stockCode, enabled]);
|
}, [stockCode, enabled]);
|
||||||
|
|
||||||
const isLoading = loading || (enabled && !hasLoaded && !error);
|
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
|
||||||
|
|
||||||
return { disclosureSchedule, loading: isLoading, error };
|
return { disclosureSchedule, loading: isLoading, error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
|
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
|
||||||
// 管理团队数据 Hook - 用于管理团队 Tab
|
// 管理团队数据 Hook - 用于管理团队 Tab
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { logger } from "@utils/logger";
|
import { logger } from "@utils/logger";
|
||||||
import axios from "@utils/axiosConfig";
|
import axios from "@utils/axiosConfig";
|
||||||
import type { Management } from "../types";
|
import type { Management } from "../types";
|
||||||
@@ -36,7 +36,10 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
|
|||||||
const [management, setManagement] = useState<Management[]>([]);
|
const [management, setManagement] = useState<Management[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
// 只有 enabled 且有 stockCode 时才请求
|
// 只有 enabled 且有 stockCode 时才请求
|
||||||
@@ -45,6 +48,18 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stockCode 变化时重置加载状态
|
||||||
|
if (lastStockCodeRef.current !== stockCode) {
|
||||||
|
hasLoadedRef.current = false;
|
||||||
|
lastStockCodeRef.current = stockCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经加载过数据,不再重新请求(Tab 切换回来时保持缓存)
|
||||||
|
if (hasLoadedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -63,7 +78,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
|
|||||||
setError("加载管理团队数据失败");
|
setError("加载管理团队数据失败");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 请求被取消时,不更新任何状态
|
// 请求被取消时,不更新任何状态
|
||||||
if (err.name === "CanceledError") {
|
if (err.name === "CanceledError") {
|
||||||
@@ -72,7 +87,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
|
|||||||
logger.error("useManagementData", "loadData", err, { stockCode });
|
logger.error("useManagementData", "loadData", err, { stockCode });
|
||||||
setError("网络请求失败");
|
setError("网络请求失败");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +97,7 @@ export const useManagementData = (options: UseManagementDataOptions): UseManagem
|
|||||||
|
|
||||||
// 派生 loading 状态:enabled 但尚未完成首次加载时,视为 loading
|
// 派生 loading 状态:enabled 但尚未完成首次加载时,视为 loading
|
||||||
// 这样可以在渲染时同步判断,避免 useEffect 异步导致的空状态闪烁
|
// 这样可以在渲染时同步判断,避免 useEffect 异步导致的空状态闪烁
|
||||||
const isLoading = loading || (enabled && !hasLoaded && !error);
|
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
|
||||||
|
|
||||||
return { management, loading: isLoading, error };
|
return { management, loading: isLoading, error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
|
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
|
||||||
// 股权结构数据 Hook - 用于股权结构 Tab
|
// 股权结构数据 Hook - 用于股权结构 Tab
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { logger } from "@utils/logger";
|
import { logger } from "@utils/logger";
|
||||||
import axios from "@utils/axiosConfig";
|
import axios from "@utils/axiosConfig";
|
||||||
import type { ActualControl, Concentration, Shareholder } from "../types";
|
import type { ActualControl, Concentration, Shareholder } from "../types";
|
||||||
@@ -42,7 +42,10 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
|
|||||||
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
|
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
// 只有 enabled 且有 stockCode 时才请求
|
// 只有 enabled 且有 stockCode 时才请求
|
||||||
@@ -51,6 +54,18 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stockCode 变化时重置加载状态
|
||||||
|
if (lastStockCodeRef.current !== stockCode) {
|
||||||
|
hasLoadedRef.current = false;
|
||||||
|
lastStockCodeRef.current = stockCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经加载过数据,不再重新请求(Tab 切换回来时保持缓存)
|
||||||
|
if (hasLoadedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -75,7 +90,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
|
|||||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||||
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// 请求被取消时,不更新任何状态
|
// 请求被取消时,不更新任何状态
|
||||||
if (err.name === "CanceledError") {
|
if (err.name === "CanceledError") {
|
||||||
@@ -84,7 +99,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
|
|||||||
logger.error("useShareholderData", "loadData", err, { stockCode });
|
logger.error("useShareholderData", "loadData", err, { stockCode });
|
||||||
setError("加载股权结构数据失败");
|
setError("加载股权结构数据失败");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setHasLoaded(true);
|
hasLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +107,7 @@ export const useShareholderData = (options: UseShareholderDataOptions): UseShare
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [stockCode, enabled]);
|
}, [stockCode, enabled]);
|
||||||
|
|
||||||
const isLoading = loading || (enabled && !hasLoaded && !error);
|
const isLoading = loading || (enabled && !hasLoadedRef.current && !error);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actualControl,
|
actualControl,
|
||||||
|
|||||||
@@ -136,5 +136,5 @@ const MarketDataSkeleton: React.FC = memo(() => (
|
|||||||
|
|
||||||
MarketDataSkeleton.displayName = 'MarketDataSkeleton';
|
MarketDataSkeleton.displayName = 'MarketDataSkeleton';
|
||||||
|
|
||||||
export { MarketDataSkeleton };
|
export { MarketDataSkeleton, SummaryCardSkeleton };
|
||||||
export default MarketDataSkeleton;
|
export default MarketDataSkeleton;
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ export { default as ThemedCard } from './ThemedCard';
|
|||||||
export { default as MarkdownRenderer } from './MarkdownRenderer';
|
export { default as MarkdownRenderer } from './MarkdownRenderer';
|
||||||
export { default as StockSummaryCard } from './StockSummaryCard';
|
export { default as StockSummaryCard } from './StockSummaryCard';
|
||||||
export { default as AnalysisModal, AnalysisContent } from './AnalysisModal';
|
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
|
period: number = DEFAULT_PERIOD
|
||||||
): UseMarketDataReturn => {
|
): UseMarketDataReturn => {
|
||||||
// 主数据状态
|
// 主数据状态
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [tradeLoading, setTradeLoading] = useState(false);
|
const [tradeLoading, setTradeLoading] = useState(false);
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
const [summary, setSummary] = useState<MarketSummary | null>(null);
|
const [summary, setSummary] = useState<MarketSummary | null>(null);
|
||||||
const [tradeData, setTradeData] = useState<TradeDayData[]>([]);
|
const [tradeData, setTradeData] = useState<TradeDayData[]>([]);
|
||||||
const [fundingData, setFundingData] = useState<FundingDayData[]>([]);
|
const [fundingData, setFundingData] = useState<FundingDayData[]>([]);
|
||||||
@@ -153,15 +154,17 @@ export const useMarketData = (
|
|||||||
if (loadedTradeData.length > 0) {
|
if (loadedTradeData.length > 0) {
|
||||||
loadRiseAnalysis(loadedTradeData);
|
loadRiseAnalysis(loadedTradeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setHasLoaded(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 取消请求不作为错误处理
|
// 请求被取消时,不更新任何状态
|
||||||
if (isCancelError(error)) return;
|
if (isCancelError(error)) {
|
||||||
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
return;
|
||||||
} finally {
|
|
||||||
// 只有当前请求没有被取消时才设置 loading 状态
|
|
||||||
if (!controller.signal.aborted) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
||||||
|
setLoading(false);
|
||||||
|
setHasLoaded(true);
|
||||||
}
|
}
|
||||||
}, [stockCode, period, loadRiseAnalysis]);
|
}, [stockCode, period, loadRiseAnalysis]);
|
||||||
|
|
||||||
@@ -363,8 +366,11 @@ export const useMarketData = (
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 派生 loading 状态:stockCode 存在但尚未完成首次加载时,视为 loading
|
||||||
|
const isLoading = loading || (!!stockCode && !hasLoaded);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading: isLoading,
|
||||||
tradeLoading,
|
tradeLoading,
|
||||||
summary,
|
summary,
|
||||||
tradeData,
|
tradeData,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useMarketData } from './hooks/useMarketData';
|
|||||||
import {
|
import {
|
||||||
ThemedCard,
|
ThemedCard,
|
||||||
StockSummaryCard,
|
StockSummaryCard,
|
||||||
|
SummaryCardSkeleton,
|
||||||
AnalysisModal,
|
AnalysisModal,
|
||||||
AnalysisContent,
|
AnalysisContent,
|
||||||
} from './components';
|
} from './components';
|
||||||
@@ -89,13 +90,12 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
}
|
}
|
||||||
}, [propStockCode, stockCode]);
|
}, [propStockCode, stockCode]);
|
||||||
|
|
||||||
// 首次渲染时加载默认 Tab(融资融券)的数据
|
// 首次挂载时加载默认 Tab(融资融券)的数据
|
||||||
|
// 注意:SubTabContainer 的 onChange 只在切换时触发,首次渲染不会触发
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 默认 Tab 是融资融券(index 0)
|
loadDataByType('funding');
|
||||||
if (activeTab === 0) {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
loadDataByType('funding');
|
}, []); // 只在首次挂载时执行
|
||||||
}
|
|
||||||
}, [loadDataByType, activeTab]);
|
|
||||||
|
|
||||||
// 处理图表点击事件
|
// 处理图表点击事件
|
||||||
const handleChartClick = useCallback(
|
const handleChartClick = useCallback(
|
||||||
@@ -137,8 +137,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
<Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}>
|
<Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}>
|
||||||
<Container maxW="container.xl" py={4}>
|
<Container maxW="container.xl" py={4}>
|
||||||
<VStack align="stretch" spacing={4}>
|
<VStack align="stretch" spacing={4}>
|
||||||
{/* 股票概览 */}
|
{/* 股票概览 - 未加载时显示骨架屏占位,避免布局跳动 */}
|
||||||
{summary && <StockSummaryCard summary={summary} theme={theme} />}
|
{summary ? <StockSummaryCard summary={summary} theme={theme} /> : <SummaryCardSkeleton />}
|
||||||
|
|
||||||
{/* 交易数据 - 日K/分钟K线(独立显示在 Tab 上方) */}
|
{/* 交易数据 - 日K/分钟K线(独立显示在 Tab 上方) */}
|
||||||
<TradeDataPanel
|
<TradeDataPanel
|
||||||
|
|||||||
Reference in New Issue
Block a user