fix: 股票详情页K线 React 310 崩溃(useMemo置于early return前) + 时间范围切换
- 修复 React error #310:displayData 的 useMemo 原置于 if(loading) early return 之后, 导致 loading 切换时 hook 数量变化触发崩溃。移到所有 early return 之前。 - K线一次性拉取 365 天,前端按 3月/6月/1年 切片展示,默认近3月。
This commit is contained in:
Generated
+22
-16
@@ -124,7 +124,7 @@ importers:
|
|||||||
version: 11.18.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 11.18.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.575.0
|
specifier: ^0.575.0
|
||||||
version: 0.575.0(react@19.2.7)
|
version: 0.575.0(react@19.2.7)
|
||||||
@@ -148,7 +148,7 @@ importers:
|
|||||||
version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -1613,11 +1613,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@4.0.7:
|
eventemitter3@4.0.1:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-MnI0l35oYL2C/c80rjJN7qu50MDx39yYE7y7oYck2YA3v+y7EaAenY8IU8AP4d1RWqE8VAKWFGSh3rfP87ll3g==}
|
||||||
|
|
||||||
fast-equals@5.4.0:
|
fast-equals@5.4.1:
|
||||||
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
@@ -1662,8 +1662,8 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
input-otp@1.4.2:
|
input-otp@1.5.0:
|
||||||
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
|
resolution: {integrity: sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
@@ -1944,11 +1944,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
|
resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
sonner@2.0.7:
|
sonner@2.0.8:
|
||||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@types/react': ^18.0.0 || ^19.0.0
|
||||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
@@ -3492,9 +3496,9 @@ snapshots:
|
|||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.1: {}
|
||||||
|
|
||||||
fast-equals@5.4.0: {}
|
fast-equals@5.4.1: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.5):
|
fdir@6.5.0(picomatch@4.0.5):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -3520,7 +3524,7 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
input-otp@1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
input-otp@1.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
@@ -3687,7 +3691,7 @@ snapshots:
|
|||||||
|
|
||||||
react-smooth@4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
react-smooth@4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-equals: 5.4.0
|
fast-equals: 5.4.1
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
@@ -3721,7 +3725,7 @@ snapshots:
|
|||||||
recharts@2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
recharts@2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
eventemitter3: 4.0.7
|
eventemitter3: 4.0.1
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
@@ -3772,10 +3776,12 @@ snapshots:
|
|||||||
|
|
||||||
seroval@1.5.4: {}
|
seroval@1.5.4: {}
|
||||||
|
|
||||||
sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
sonner@2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
|||||||
+42
-14
@@ -86,6 +86,7 @@ function StockDetail() {
|
|||||||
const [fundFlowLoading, setFundFlowLoading] = useState(false);
|
const [fundFlowLoading, setFundFlowLoading] = useState(false);
|
||||||
const [fundFlowPeriod, setFundFlowPeriod] = useState(21);
|
const [fundFlowPeriod, setFundFlowPeriod] = useState(21);
|
||||||
const [dailyTableDays, setDailyTableDays] = useState<number>(7);
|
const [dailyTableDays, setDailyTableDays] = useState<number>(7);
|
||||||
|
const [chartRange, setChartRange] = useState(90); // 默认近3月
|
||||||
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 [inCollection, setInCollection] = useState(false);
|
const [inCollection, setInCollection] = useState(false);
|
||||||
@@ -175,9 +176,8 @@ function StockDetail() {
|
|||||||
|
|
||||||
// 阶段2:并行加载历史K线 + 资金流向(后置加载,不阻塞首屏)
|
// 阶段2:并行加载历史K线 + 资金流向(后置加载,不阻塞首屏)
|
||||||
const addedDate = new Date(addedAt);
|
const addedDate = new Date(addedAt);
|
||||||
const now = new Date();
|
// 一次性拉取最多 1 年(365天)K线,展示范围由 chartRange 前端切片
|
||||||
const daysSinceAdded = Math.floor((now.getTime() - addedDate.getTime()) / (1000 * 60 * 60 * 24));
|
const chartDays = 365;
|
||||||
const chartDays = Math.min(Math.max(90, daysSinceAdded), 120);
|
|
||||||
|
|
||||||
setChartLoading(true);
|
setChartLoading(true);
|
||||||
setFundFlowLoading(true);
|
setFundFlowLoading(true);
|
||||||
@@ -288,6 +288,14 @@ function StockDetail() {
|
|||||||
const roe = latest.indicators["加权净资产收益率"];
|
const roe = latest.indicators["加权净资产收益率"];
|
||||||
return typeof roe === "number" ? roe : null;
|
return typeof roe === "number" ? roe : null;
|
||||||
}, [financialData]);
|
}, [financialData]);
|
||||||
|
|
||||||
|
// 展示数据 = 按 chartRange 切片最近 N 天(chartData 存了全年,这里只取窗口)
|
||||||
|
// 必须在所有 early return 之前调用,保证 hook 顺序一致
|
||||||
|
const displayData = useMemo(() => {
|
||||||
|
if (chartData.length <= chartRange) return chartData;
|
||||||
|
return chartData.slice(chartData.length - chartRange);
|
||||||
|
}, [chartData, chartRange]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@@ -334,19 +342,19 @@ function StockDetail() {
|
|||||||
// 找到添加日期在图表中的位置
|
// 找到添加日期在图表中的位置
|
||||||
const addedDateObj = stockInfo ? new Date(stockInfo.addedAt) : null;
|
const addedDateObj = stockInfo ? new Date(stockInfo.addedAt) : null;
|
||||||
const addedDateStr = addedDateObj ? addedDateObj.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }) : "";
|
const addedDateStr = addedDateObj ? addedDateObj.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }) : "";
|
||||||
const addedDataPoint = inCollection ? chartData.find(d => d.isAddedDate) : undefined;
|
const addedDataPoint = inCollection ? displayData.find(d => d.isAddedDate) : undefined;
|
||||||
|
|
||||||
// 计算X轴刻度:保证添加日期始终有标签
|
// 计算X轴刻度:保证添加日期始终有标签
|
||||||
const tickDates = (() => {
|
const tickDates = (() => {
|
||||||
if (chartData.length === 0) return [];
|
if (displayData.length === 0) return [];
|
||||||
const first = chartData[0].date;
|
const first = displayData[0].date;
|
||||||
const last = chartData[chartData.length - 1].date;
|
const last = displayData[displayData.length - 1].date;
|
||||||
const ticks = [first];
|
const ticks = [first];
|
||||||
const added = addedDataPoint?.date;
|
const added = addedDataPoint?.date;
|
||||||
const targetCount = Math.min(7, chartData.length);
|
const targetCount = Math.min(7, displayData.length);
|
||||||
const step = Math.max(1, Math.floor((chartData.length - 1) / (targetCount - 2)));
|
const step = Math.max(1, Math.floor((displayData.length - 1) / (targetCount - 2)));
|
||||||
for (let i = step; i < chartData.length - 1; i += step) {
|
for (let i = step; i < displayData.length - 1; i += step) {
|
||||||
ticks.push(chartData[i].date);
|
ticks.push(displayData[i].date);
|
||||||
}
|
}
|
||||||
if (added && !ticks.includes(added)) {
|
if (added && !ticks.includes(added)) {
|
||||||
ticks.push(added);
|
ticks.push(added);
|
||||||
@@ -516,13 +524,33 @@ function StockDetail() {
|
|||||||
<CardTitle className="text-base md:text-lg">
|
<CardTitle className="text-base md:text-lg">
|
||||||
K线图
|
K线图
|
||||||
<span className="text-xs md:text-sm font-normal text-muted-foreground ml-2">
|
<span className="text-xs md:text-sm font-normal text-muted-foreground ml-2">
|
||||||
{chartLoading ? "(加载中...)" : `(${chartData.length}个交易日)`}
|
{chartLoading ? "(加载中...)" : `(${displayData.length}个交易日)`}
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
{/* 时间范围:3月 / 6月 / 1年 */}
|
||||||
|
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
|
||||||
|
{[
|
||||||
|
{ key: 90, label: "3月" },
|
||||||
|
{ key: 180, label: "6月" },
|
||||||
|
{ key: 365, label: "1年" },
|
||||||
|
].map((r) => (
|
||||||
|
<button
|
||||||
|
key={r.key}
|
||||||
|
onClick={() => setChartRange(r.key)}
|
||||||
|
className={`px-2.5 py-1 transition-colors ${
|
||||||
|
chartRange === r.key ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{r.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<span className="text-xs text-muted-foreground hidden sm:inline">
|
<span className="text-xs text-muted-foreground hidden sm:inline">
|
||||||
双指缩放 · 拖动选区查看
|
双指缩放 · 拖动选区查看
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 md:px-6 pb-2 md:pb-6">
|
<CardContent className="px-2 md:px-6 pb-2 md:pb-6">
|
||||||
{chartLoading ? (
|
{chartLoading ? (
|
||||||
@@ -540,7 +568,7 @@ function StockDetail() {
|
|||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<ComposedChart
|
<ComposedChart
|
||||||
data={chartData}
|
data={displayData}
|
||||||
margin={{ top: 10, right: 8, left: 0, bottom: 0 }}
|
margin={{ top: 10, right: 8, left: 0, bottom: 0 }}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" opacity={0.5} />
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" opacity={0.5} />
|
||||||
@@ -554,7 +582,7 @@ function StockDetail() {
|
|||||||
height={56}
|
height={56}
|
||||||
tick={(props: any) => {
|
tick={(props: any) => {
|
||||||
const { x, y, payload } = props;
|
const { x, y, payload } = props;
|
||||||
const dataPoint = chartData.find(d => d.date === payload.value);
|
const dataPoint = displayData.find(d => d.date === payload.value);
|
||||||
const isAddedDate = inCollection && dataPoint?.isAddedDate;
|
const isAddedDate = inCollection && dataPoint?.isAddedDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user