fix(hooks): 添加 AbortController 解决竞态条件问题
在以下 Hook 中添加请求取消逻辑,防止快速切换股票时旧数据覆盖新数据: - useBasicInfo - useShareholderData - useManagementData - useBranchesData - useAnnouncementsData - useDisclosureData - useStockQuoteData 修复前:stockCode 变化时,旧请求可能后返回,覆盖新数据 修复后:cleanup 时取消旧请求,确保数据一致性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts
|
||||
// 公告数据 Hook - 用于公司公告 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Announcement } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useAnnouncementsData = (stockCode?: string): UseAnnouncementsDataRe
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Announcement[]>>(
|
||||
`/api/stock/${stockCode}/announcements?limit=20`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setAnnouncements(result.data);
|
||||
} else {
|
||||
setError("加载公告数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Announcement[]>>(
|
||||
`/api/stock/${stockCode}/announcements?limit=20`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setAnnouncements(result.data);
|
||||
} else {
|
||||
setError("加载公告数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useAnnouncementsData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { announcements, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts
|
||||
// 公司基本信息 Hook - 用于 CompanyHeaderCard
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { BasicInfo } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useBasicInfo = (stockCode?: string): UseBasicInfoResult => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<BasicInfo>>(
|
||||
`/api/stock/${stockCode}/basic-info`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
} else {
|
||||
setError("加载基本信息失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<BasicInfo>>(
|
||||
`/api/stock/${stockCode}/basic-info`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
} else {
|
||||
setError("加载基本信息失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useBasicInfo", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useBasicInfo", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { basicInfo, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts
|
||||
// 分支机构数据 Hook - 用于分支机构 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Branch } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useBranchesData = (stockCode?: string): UseBranchesDataResult => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Branch[]>>(
|
||||
`/api/stock/${stockCode}/branches`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setBranches(result.data);
|
||||
} else {
|
||||
setError("加载分支机构数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Branch[]>>(
|
||||
`/api/stock/${stockCode}/branches`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setBranches(result.data);
|
||||
} else {
|
||||
setError("加载分支机构数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useBranchesData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useBranchesData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { branches, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts
|
||||
// 披露日程数据 Hook - 用于工商信息 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { DisclosureSchedule } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useDisclosureData = (stockCode?: string): UseDisclosureDataResult =
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<DisclosureSchedule[]>>(
|
||||
`/api/stock/${stockCode}/disclosure-schedule`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setDisclosureSchedule(result.data);
|
||||
} else {
|
||||
setError("加载披露日程数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<DisclosureSchedule[]>>(
|
||||
`/api/stock/${stockCode}/disclosure-schedule`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setDisclosureSchedule(result.data);
|
||||
} else {
|
||||
setError("加载披露日程数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useDisclosureData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { disclosureSchedule, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts
|
||||
// 管理团队数据 Hook - 用于管理团队 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { Management } from "../types";
|
||||
@@ -26,33 +26,38 @@ export const useManagementData = (stockCode?: string): UseManagementDataResult =
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Management[]>>(
|
||||
`/api/stock/${stockCode}/management?active_only=true`
|
||||
);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (result.success) {
|
||||
setManagement(result.data);
|
||||
} else {
|
||||
setError("加载管理团队数据失败");
|
||||
try {
|
||||
const { data: result } = await axios.get<ApiResponse<Management[]>>(
|
||||
`/api/stock/${stockCode}/management?active_only=true`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setManagement(result.data);
|
||||
} else {
|
||||
setError("加载管理团队数据失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useManagementData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("useManagementData", "loadData", err, { stockCode });
|
||||
setError("网络请求失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return { management, loading, error };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts
|
||||
// 股权结构数据 Hook - 用于股权结构 Tab
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
import type { ActualControl, Concentration, Shareholder } from "../types";
|
||||
@@ -32,40 +32,44 @@ export const useShareholderData = (stockCode?: string): UseShareholderDataResult
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const [
|
||||
{ data: actualRes },
|
||||
{ data: concentrationRes },
|
||||
{ data: shareholdersRes },
|
||||
{ data: circulationRes },
|
||||
] = await Promise.all([
|
||||
axios.get<ApiResponse<ActualControl[]>>(`/api/stock/${stockCode}/actual-control`),
|
||||
axios.get<ApiResponse<Concentration[]>>(`/api/stock/${stockCode}/concentration`),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-shareholders?limit=10`),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-circulation-shareholders?limit=10`),
|
||||
]);
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (actualRes.success) setActualControl(actualRes.data);
|
||||
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||
} catch (err) {
|
||||
logger.error("useShareholderData", "loadData", err, { stockCode });
|
||||
setError("加载股权结构数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
try {
|
||||
const [
|
||||
{ data: actualRes },
|
||||
{ data: concentrationRes },
|
||||
{ data: shareholdersRes },
|
||||
{ data: circulationRes },
|
||||
] = await Promise.all([
|
||||
axios.get<ApiResponse<ActualControl[]>>(`/api/stock/${stockCode}/actual-control`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Concentration[]>>(`/api/stock/${stockCode}/concentration`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-shareholders?limit=10`, { signal: controller.signal }),
|
||||
axios.get<ApiResponse<Shareholder[]>>(`/api/stock/${stockCode}/top-circulation-shareholders?limit=10`, { signal: controller.signal }),
|
||||
]);
|
||||
|
||||
if (actualRes.success) setActualControl(actualRes.data);
|
||||
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||
} catch (err: any) {
|
||||
if (err.name === "CanceledError") return;
|
||||
logger.error("useShareholderData", "loadData", err, { stockCode });
|
||||
setError("加载股权结构数据失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
return () => controller.abort();
|
||||
}, [stockCode]);
|
||||
|
||||
return {
|
||||
actualControl,
|
||||
|
||||
@@ -73,24 +73,18 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
const [basicLoading, setBasicLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 获取行情数据
|
||||
const fetchQuote = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
setQuoteData(null);
|
||||
return;
|
||||
}
|
||||
// 用于手动刷新的 ref
|
||||
const refetchRef = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 获取行情数据
|
||||
setQuoteLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
||||
const quotes = await stockService.getQuotes([stockCode]);
|
||||
|
||||
// API 返回格式: { [stockCode]: quoteData }
|
||||
const quoteResult = quotes?.[stockCode] || quotes;
|
||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
||||
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} catch (err) {
|
||||
@@ -100,49 +94,85 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
} finally {
|
||||
setQuoteLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 获取基本信息
|
||||
const fetchBasicInfo = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
setBasicInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取基本信息
|
||||
setBasicLoading(true);
|
||||
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`);
|
||||
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
// 基本信息获取失败不影响主流程,只记录日志
|
||||
} finally {
|
||||
setBasicLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// stockCode 变化时重新获取数据
|
||||
// stockCode 变化时重新获取数据(带取消支持)
|
||||
useEffect(() => {
|
||||
fetchQuote();
|
||||
fetchBasicInfo();
|
||||
}, [fetchQuote, fetchBasicInfo]);
|
||||
if (!stockCode) {
|
||||
setQuoteData(null);
|
||||
setBasicInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动刷新
|
||||
const refetch = useCallback(() => {
|
||||
fetchQuote();
|
||||
fetchBasicInfo();
|
||||
}, [fetchQuote, fetchBasicInfo]);
|
||||
const controller = new AbortController();
|
||||
let isCancelled = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
// 获取行情数据
|
||||
setQuoteLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
||||
const quotes = await stockService.getQuotes([stockCode]);
|
||||
if (isCancelled) return;
|
||||
const quoteResult = quotes?.[stockCode] || quotes;
|
||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||
setError('获取行情数据失败');
|
||||
setQuoteData(null);
|
||||
} finally {
|
||||
if (!isCancelled) setQuoteLoading(false);
|
||||
}
|
||||
|
||||
// 获取基本信息
|
||||
setBasicLoading(true);
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (isCancelled) return;
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
} finally {
|
||||
if (!isCancelled) setBasicLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [stockCode]);
|
||||
|
||||
return {
|
||||
quoteData,
|
||||
basicInfo,
|
||||
isLoading: quoteLoading || basicLoading,
|
||||
error,
|
||||
refetch,
|
||||
refetch: refetchRef,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user