Skip to content

Commit e801170

Browse files
ZhuLinsenaibot-hub
authored andcommitted
fix: 大盘复盘结果区滚动锁定问题 (ZhuLinsen#1266) (ZhuLinsen#1270)
* fix(issue-1266): [bug]-新版本大盘复盘后,下面的界面滑不上去了 * fix(review-feedback-1270): address latest review comments
1 parent dbfdcf0 commit e801170

3 files changed

Lines changed: 158 additions & 46 deletions

File tree

apps/dsa-web/src/pages/HomePage.tsx

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const HomePage: React.FC = () => {
3131
const [marketReviewReport, setMarketReviewReport] = useState<string | null>(null);
3232
const [marketReviewReportCopied, setMarketReviewReportCopied] = useState(false);
3333
const marketReviewPollTimer = useRef<number | null>(null);
34+
const dashboardScrollRef = useRef<HTMLElement | null>(null);
3435

3536
const stopMarketReviewPolling = useCallback(() => {
3637
if (marketReviewPollTimer.current !== null) {
@@ -39,6 +40,20 @@ const HomePage: React.FC = () => {
3940
}
4041
}, []);
4142

43+
const scrollMarketReviewFeedbackIntoView = useCallback(() => {
44+
const scrollContainer = dashboardScrollRef.current;
45+
if (!scrollContainer) {
46+
return;
47+
}
48+
49+
if (typeof scrollContainer.scrollTo === 'function') {
50+
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
51+
return;
52+
}
53+
54+
scrollContainer.scrollTop = 0;
55+
}, []);
56+
4257
useEffect(() => stopMarketReviewPolling, [stopMarketReviewPolling]);
4358
const [setupStatus, setSetupStatus] = useState<SetupStatusResponse | null>(null);
4459

@@ -187,6 +202,7 @@ const HomePage: React.FC = () => {
187202
title: '大盘复盘已超时',
188203
message: '任务长时间未返回最终结果,请在任务列表/历史中查看。',
189204
});
205+
scrollMarketReviewFeedbackIntoView();
190206
return false;
191207
}
192208

@@ -219,6 +235,7 @@ const HomePage: React.FC = () => {
219235
message: marketReviewText ? '大盘复盘任务已完成,结果如下:' : '大盘复盘任务已完成,结果已生成并按配置推送。',
220236
});
221237
setMarketReviewError(null);
238+
scrollMarketReviewFeedbackIntoView();
222239
return false;
223240
}
224241

@@ -237,6 +254,7 @@ const HomePage: React.FC = () => {
237254
}),
238255
);
239256
setMarketReviewNotice(null);
257+
scrollMarketReviewFeedbackIntoView();
240258
return false;
241259
}
242260

@@ -247,6 +265,7 @@ const HomePage: React.FC = () => {
247265
title: '大盘复盘状态异常',
248266
message: `收到未知任务状态:${status.status}`,
249267
});
268+
scrollMarketReviewFeedbackIntoView();
250269
return false;
251270
} catch (err: unknown) {
252271
const parsed = getParsedApiError(err);
@@ -255,6 +274,7 @@ const HomePage: React.FC = () => {
255274
setMarketReviewReport(null);
256275
setMarketReviewError(parsed);
257276
setMarketReviewNotice(null);
277+
scrollMarketReviewFeedbackIntoView();
258278
return false;
259279
}
260280
return true;
@@ -273,32 +293,35 @@ const HomePage: React.FC = () => {
273293
}, intervalMs);
274294
}
275295
},
276-
[stopMarketReviewPolling],
296+
[scrollMarketReviewFeedbackIntoView, stopMarketReviewPolling],
277297
);
278298

279299
const handleTriggerMarketReview = useCallback(async () => {
280300
setIsSubmittingMarketReview(true);
281301
setMarketReviewNotice(null);
282302
setMarketReviewError(null);
283303
setMarketReviewReport(null);
304+
scrollMarketReviewFeedbackIntoView();
284305
try {
285306
const result = await analysisApi.triggerMarketReview({ sendNotification: notify });
286307
setMarketReviewNotice({
287308
variant: 'success',
288309
title: '大盘复盘已提交',
289310
message: result.message,
290311
});
312+
scrollMarketReviewFeedbackIntoView();
291313

292314
if (result.taskId) {
293315
await pollMarketReviewStatus(result.taskId);
294316
}
295317
} catch (err: unknown) {
296318
setMarketReviewError(getParsedApiError(err));
297319
setMarketReviewNotice(null);
320+
scrollMarketReviewFeedbackIntoView();
298321
} finally {
299322
setIsSubmittingMarketReview(false);
300323
}
301-
}, [notify, pollMarketReviewStatus]);
324+
}, [notify, pollMarketReviewStatus, scrollMarketReviewFeedbackIntoView]);
302325

303326
const handleCopyMarketReviewReport = useCallback(() => {
304327
if (!marketReviewReport) {
@@ -479,48 +502,6 @@ const HomePage: React.FC = () => {
479502
</div>
480503
) : null}
481504

482-
{marketReviewNotice ? (
483-
<div className="px-3 pb-2 md:px-4">
484-
<InlineAlert
485-
variant={marketReviewNotice.variant}
486-
title={marketReviewNotice.title}
487-
message={marketReviewNotice.message}
488-
className="rounded-xl px-3 py-2 text-xs shadow-none"
489-
/>
490-
</div>
491-
) : null}
492-
493-
{marketReviewError ? (
494-
<div className="px-3 pb-2 md:px-4">
495-
<ApiErrorAlert
496-
error={marketReviewError}
497-
className="mb-1"
498-
onDismiss={() => setMarketReviewError(null)}
499-
/>
500-
</div>
501-
) : null}
502-
503-
{marketReviewReport ? (
504-
<div className="px-3 pb-2 md:px-4">
505-
<div className="rounded-xl border border-subtle bg-surface/70 px-3 py-3 text-xs text-secondary-text shadow-sm">
506-
<div className="mb-2 flex items-center justify-between gap-2">
507-
<p className="font-semibold text-foreground">大盘复盘报告</p>
508-
<button
509-
type="button"
510-
className="home-surface-button h-7 rounded-md px-3 py-1 text-xs text-foreground"
511-
disabled={marketReviewReportCopied}
512-
onClick={() => void handleCopyMarketReviewReport()}
513-
>
514-
{marketReviewReportCopied ? '已复制' : '复制'}
515-
</button>
516-
</div>
517-
<pre className="max-h-64 overflow-x-auto overflow-y-auto whitespace-pre-wrap break-words rounded-lg bg-background px-3 py-2 leading-relaxed">
518-
{marketReviewReport}
519-
</pre>
520-
</div>
521-
</div>
522-
) : null}
523-
524505
<div className="flex-1 flex min-h-0 overflow-hidden">
525506
<div className="hidden min-h-0 w-64 shrink-0 flex-col overflow-hidden pl-4 pb-4 md:flex lg:w-72">
526507
{sidebarContent}
@@ -538,7 +519,54 @@ const HomePage: React.FC = () => {
538519
</div>
539520
) : null}
540521

541-
<section className="flex-1 min-w-0 min-h-0 overflow-x-auto overflow-y-auto px-3 pb-4 md:px-6 touch-pan-y">
522+
<section
523+
ref={dashboardScrollRef}
524+
data-testid="home-dashboard-scroll"
525+
className="flex-1 min-w-0 min-h-0 overflow-x-auto overflow-y-auto px-3 pb-4 md:px-6 touch-pan-y"
526+
>
527+
{marketReviewNotice ? (
528+
<div className="mb-3">
529+
<InlineAlert
530+
variant={marketReviewNotice.variant}
531+
title={marketReviewNotice.title}
532+
message={marketReviewNotice.message}
533+
className="rounded-xl px-3 py-2 text-xs shadow-none"
534+
/>
535+
</div>
536+
) : null}
537+
538+
{marketReviewError ? (
539+
<div className="mb-3">
540+
<ApiErrorAlert
541+
error={marketReviewError}
542+
className="mb-1"
543+
onDismiss={() => setMarketReviewError(null)}
544+
/>
545+
</div>
546+
) : null}
547+
548+
{marketReviewReport ? (
549+
<div className="mb-3 rounded-xl border border-subtle bg-surface/70 px-3 py-3 text-xs text-secondary-text shadow-sm">
550+
<div className="mb-2 flex items-center justify-between gap-2">
551+
<p className="font-semibold text-foreground">大盘复盘报告</p>
552+
<button
553+
type="button"
554+
className="home-surface-button h-7 rounded-md px-3 py-1 text-xs text-foreground"
555+
disabled={marketReviewReportCopied}
556+
onClick={() => void handleCopyMarketReviewReport()}
557+
>
558+
{marketReviewReportCopied ? '已复制' : '复制'}
559+
</button>
560+
</div>
561+
<pre
562+
data-testid="market-review-report"
563+
className="overflow-x-auto whitespace-pre-wrap break-words rounded-lg bg-background px-3 py-2 leading-relaxed"
564+
>
565+
{marketReviewReport}
566+
</pre>
567+
</div>
568+
) : null}
569+
542570
{error ? (
543571
<ApiErrorAlert
544572
error={error}

apps/dsa-web/src/pages/__tests__/HomePage.test.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ describe('HomePage', () => {
117117
expect(dashboard.className).toContain('lg:h-[calc(100vh-2rem)]');
118118
expect(dashboard.firstElementChild?.className).toContain('min-h-0');
119119
expect(dashboard.querySelector('.flex-1.flex.min-h-0.overflow-hidden')).toBeTruthy();
120+
expect(screen.getByTestId('home-dashboard-scroll')).toBeInTheDocument();
120121
expect(screen.getByPlaceholderText('输入股票代码或名称,如 600519、贵州茅台、AAPL')).toBeInTheDocument();
121122
expect(await screen.findByText('趋势维持强势')).toBeInTheDocument();
122123
expect(
@@ -208,6 +209,89 @@ describe('HomePage', () => {
208209
expect(analysisApi.getStatus).toHaveBeenCalledWith('task-1');
209210
});
210211

212+
it('scrolls the dashboard to market review feedback after toolbar clicks', async () => {
213+
vi.mocked(historyApi.getList).mockResolvedValue({
214+
total: 1,
215+
page: 1,
216+
limit: 20,
217+
items: [historyItem],
218+
});
219+
vi.mocked(historyApi.getDetail).mockResolvedValue(historyReport);
220+
vi.mocked(analysisApi.triggerMarketReview).mockResolvedValue({
221+
status: 'accepted',
222+
sendNotification: true,
223+
message: '大盘复盘任务已提交',
224+
taskId: 'task-1',
225+
});
226+
vi.mocked(analysisApi.getStatus).mockResolvedValue({
227+
taskId: 'task-1',
228+
status: 'completed',
229+
marketReviewReport: '市场复盘报告示例文本',
230+
});
231+
232+
render(
233+
<MemoryRouter>
234+
<HomePage />
235+
</MemoryRouter>,
236+
);
237+
238+
await screen.findByText('趋势维持强势');
239+
const dashboardScroll = screen.getByTestId('home-dashboard-scroll');
240+
const scrollToMock = vi.fn(function scrollTo(this: HTMLElement, options?: ScrollToOptions) {
241+
if (typeof options?.top === 'number') {
242+
this.scrollTop = options.top;
243+
}
244+
});
245+
Object.defineProperty(dashboardScroll, 'scrollTo', {
246+
configurable: true,
247+
value: scrollToMock,
248+
});
249+
dashboardScroll.scrollTop = 480;
250+
251+
fireEvent.click(screen.getByRole('button', { name: '大盘复盘' }));
252+
253+
await waitFor(() => {
254+
expect(scrollToMock).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
255+
});
256+
expect(dashboardScroll.scrollTop).toBe(0);
257+
expect(await screen.findByText('大盘复盘已完成')).toBeInTheDocument();
258+
});
259+
260+
it('keeps market review results in the main dashboard scroll area', async () => {
261+
vi.mocked(historyApi.getList).mockResolvedValue({
262+
total: 0,
263+
page: 1,
264+
limit: 20,
265+
items: [],
266+
});
267+
vi.mocked(analysisApi.triggerMarketReview).mockResolvedValue({
268+
status: 'accepted',
269+
sendNotification: true,
270+
message: '大盘复盘任务已提交',
271+
taskId: 'task-1',
272+
});
273+
vi.mocked(analysisApi.getStatus).mockResolvedValue({
274+
taskId: 'task-1',
275+
status: 'completed',
276+
marketReviewReport: Array.from({ length: 30 }, (_, index) => `第 ${index + 1} 行复盘内容`).join('\n'),
277+
});
278+
279+
render(
280+
<MemoryRouter>
281+
<HomePage />
282+
</MemoryRouter>,
283+
);
284+
285+
fireEvent.click(await screen.findByRole('button', { name: '大盘复盘' }));
286+
287+
const dashboardScroll = screen.getByTestId('home-dashboard-scroll');
288+
const marketReviewReport = await screen.findByTestId('market-review-report');
289+
expect(dashboardScroll).toContainElement(marketReviewReport);
290+
expect(marketReviewReport.className).not.toContain('max-h-64');
291+
expect(marketReviewReport.className).not.toContain('overflow-y-auto');
292+
expect(await screen.findByText('开始分析')).toBeInTheDocument();
293+
});
294+
211295
it('shows first-run setup gaps and links to settings', async () => {
212296
vi.mocked(historyApi.getList).mockResolvedValue({
213297
total: 0,

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
6767
- [修复] Windows NSIS 自动更新在安装尝试未切换桌面端版本时跳过自动恢复,避免失败或取消安装后误回滚用户运行时数据。
6868
- [修复] 同版本启动时清理未生效的自动更新备份目录,避免后续升级误将旧 `.dsa-desktop-update-backup/runtime-state.json` 的运行时文件再次恢复到新版本。
6969
- [修复] 清理提交中的临时探测文件(`node_modules_exists.txt``node_modules_ls_check.txt`),避免污染桌面/前端改动范围。
70-
7170
- [新功能] Web 系统设置页开放 `.env` 配置备份导入/导出,复用键级覆盖、配置版本冲突保护和重载链路;Web 端在 `ADMIN_AUTH_ENABLED=false` 时该入口为禁用状态。
7271
- [chore] 精简仓库根目录:将文档图片资源迁入 `docs/assets/`,将东方财富请求补丁迁入 `src/patches/`,并下移 CI 专用依赖文件与技能适配服务。
7372
- [文档] 更新多语言 README 首页浅色工作台 GIF,并精简功能特性表,保留原有赞助商、快速开始和推送效果结构。
@@ -76,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7675
- [修复] Docker 挂载的 `logs` 目录不可写时启动日志自动降级到控制台输出,并补充非 root 容器目录权限说明。
7776
- [修复] 修正分析报告 API 构建策略点位时数值字段未归一为字符串的问题,避免策略价格触发响应 DTO 类型校验失败。
7877
- [修复] Docker 启动入口自动修复 `data` / `logs` / `reports` 挂载目录权限并降权运行,文档化的 Compose `exec` 手动命令显式使用 `dsa` 用户,避免普通部署需要手动 `chown` / `chmod`
78+
- [修复] Web 首页大盘复盘结果改由主内容滚动区承载,避免 loading 切换到长结果后下方报告区域被截断或无法继续滚动。
7979

8080
## [3.16.0] - 2026-05-10
8181

0 commit comments

Comments
 (0)