mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: refresh report after reanalysis completes (#1809)
* fix: refresh report after reanalysis completes * fix: preserve completed report selection * fix(review-feedback-1809): 修复 * fix: type completed refresh test promise
This commit is contained in:
@@ -108,8 +108,9 @@ describe('useDashboardLifecycle', () => {
|
||||
expect(removeTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes history and removes completed tasks after the grace window', () => {
|
||||
it('refreshes completed task history and removes completed tasks after the grace window', () => {
|
||||
const refreshHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshHistoryForCompletedTask = vi.fn().mockResolvedValue(undefined);
|
||||
const syncTaskUpdated = vi.fn();
|
||||
const removeTask = vi.fn();
|
||||
|
||||
@@ -117,6 +118,7 @@ describe('useDashboardLifecycle', () => {
|
||||
useDashboardLifecycle({
|
||||
loadInitialHistory: vi.fn().mockResolvedValue(undefined),
|
||||
refreshHistory,
|
||||
refreshHistoryForCompletedTask,
|
||||
refreshActiveTasks: vi.fn().mockResolvedValue(undefined),
|
||||
syncTaskCreated: vi.fn(),
|
||||
syncTaskUpdated,
|
||||
@@ -134,7 +136,8 @@ describe('useDashboardLifecycle', () => {
|
||||
});
|
||||
|
||||
expect(syncTaskUpdated).toHaveBeenCalledWith(completedTask);
|
||||
expect(refreshHistory).toHaveBeenCalledWith(true);
|
||||
expect(refreshHistoryForCompletedTask).toHaveBeenCalledWith(completedTask);
|
||||
expect(refreshHistory).not.toHaveBeenCalledWith(true);
|
||||
expect(defaultMocks.refreshMarketReviewHistory).toHaveBeenCalledWith(true);
|
||||
|
||||
act(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTaskStream } from './useTaskStream';
|
||||
type UseDashboardLifecycleOptions = {
|
||||
loadInitialHistory: () => Promise<void>;
|
||||
refreshHistory: (silent?: boolean) => Promise<void>;
|
||||
refreshHistoryForCompletedTask?: (task: TaskInfo) => Promise<void>;
|
||||
refreshActiveTasks: () => Promise<void>;
|
||||
loadStockBar: () => Promise<void>;
|
||||
refreshStockBar: () => Promise<void>;
|
||||
@@ -20,6 +21,7 @@ type UseDashboardLifecycleOptions = {
|
||||
export function useDashboardLifecycle({
|
||||
loadInitialHistory,
|
||||
refreshHistory,
|
||||
refreshHistoryForCompletedTask,
|
||||
refreshActiveTasks,
|
||||
loadStockBar,
|
||||
refreshStockBar,
|
||||
@@ -102,7 +104,11 @@ export function useDashboardLifecycle({
|
||||
},
|
||||
onTaskCompleted: (task) => {
|
||||
syncTaskUpdated(task);
|
||||
void refreshHistory(true);
|
||||
if (refreshHistoryForCompletedTask) {
|
||||
void refreshHistoryForCompletedTask(task);
|
||||
} else {
|
||||
void refreshHistory(true);
|
||||
}
|
||||
void refreshStockBar();
|
||||
void refreshMarketReviewHistory?.(true);
|
||||
scheduleTaskRemoval(task.taskId, 2_000);
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useHomeDashboardState() {
|
||||
clearError: state.clearError,
|
||||
loadInitialHistory: state.loadInitialHistory,
|
||||
refreshHistory: state.refreshHistory,
|
||||
refreshHistoryForCompletedTask: state.refreshHistoryForCompletedTask,
|
||||
loadMoreHistory: state.loadMoreHistory,
|
||||
loadMarketReviewHistory: state.loadMarketReviewHistory,
|
||||
refreshMarketReviewHistory: state.refreshMarketReviewHistory,
|
||||
|
||||
@@ -113,6 +113,7 @@ const HomePage: React.FC = () => {
|
||||
clearError,
|
||||
loadInitialHistory,
|
||||
refreshHistory,
|
||||
refreshHistoryForCompletedTask,
|
||||
loadMarketReviewHistory,
|
||||
refreshMarketReviewHistory,
|
||||
selectHistoryItem,
|
||||
@@ -359,6 +360,7 @@ const HomePage: React.FC = () => {
|
||||
useDashboardLifecycle({
|
||||
loadInitialHistory,
|
||||
refreshHistory,
|
||||
refreshHistoryForCompletedTask,
|
||||
loadMarketReviewHistory,
|
||||
refreshMarketReviewHistory,
|
||||
loadStockBar,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { analysisApi, DuplicateTaskError } from '../../api/analysis';
|
||||
import { historyApi } from '../../api/history';
|
||||
import type { TaskInfo, TaskListResponse } from '../../types/analysis';
|
||||
import type { AnalysisReport, HistoryListResponse, TaskInfo, TaskListResponse } from '../../types/analysis';
|
||||
import { getRecentStartDate, getTodayInShanghai } from '../../utils/format';
|
||||
import { useStockPoolStore } from '../stockPoolStore';
|
||||
|
||||
@@ -562,6 +562,339 @@ describe('stockPoolStore', () => {
|
||||
expect(state.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('selects the newest report for the completed task stock during silent refresh', async () => {
|
||||
const latestItem = {
|
||||
...historyItem,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
const latestReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [historyItem],
|
||||
selectedReport: historyReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList).mockResolvedValue({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
vi.mocked(historyApi.getDetail).mockResolvedValue(latestReport);
|
||||
|
||||
await useStockPoolStore.getState().refreshHistoryForCompletedTask(createTask({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}));
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(historyApi.getDetail).toHaveBeenCalledWith(2);
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([2, 1]);
|
||||
expect(state.selectedReport?.meta.id).toBe(2);
|
||||
});
|
||||
|
||||
it('selects the completed-task report after an overlapping refresh supersedes the original request', async () => {
|
||||
const latestItem = {
|
||||
...historyItem,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
const latestReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
},
|
||||
};
|
||||
const completedRefresh = createDeferred<HistoryListResponse>();
|
||||
const overlappingRefresh = createDeferred<HistoryListResponse>();
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [historyItem],
|
||||
selectedReport: historyReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList)
|
||||
.mockReturnValueOnce(completedRefresh.promise)
|
||||
.mockReturnValueOnce(overlappingRefresh.promise);
|
||||
vi.mocked(historyApi.getDetail).mockResolvedValue(latestReport);
|
||||
|
||||
const completedRefreshPromise = useStockPoolStore.getState().refreshHistoryForCompletedTask(createTask({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}));
|
||||
const overlappingRefreshPromise = useStockPoolStore.getState().refreshHistory(true);
|
||||
|
||||
overlappingRefresh.resolve({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
await overlappingRefreshPromise;
|
||||
|
||||
completedRefresh.resolve({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
await completedRefreshPromise;
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(historyApi.getDetail).toHaveBeenCalledTimes(1);
|
||||
expect(historyApi.getDetail).toHaveBeenCalledWith(2);
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([2, 1]);
|
||||
expect(state.selectedReport?.meta.id).toBe(2);
|
||||
});
|
||||
|
||||
it('selects the newest completed-task report when stock codes use equivalent aliases', async () => {
|
||||
const olderTencentItem = {
|
||||
...historyItem,
|
||||
id: 10,
|
||||
queryId: 'q-10',
|
||||
stockCode: 'HK00700',
|
||||
stockName: '腾讯控股',
|
||||
};
|
||||
const olderTencentReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 10,
|
||||
queryId: 'q-10',
|
||||
stockCode: 'HK00700',
|
||||
stockName: '腾讯控股',
|
||||
},
|
||||
};
|
||||
const latestTencentItem = {
|
||||
...olderTencentItem,
|
||||
id: 11,
|
||||
queryId: 'q-11',
|
||||
stockCode: '00700.HK',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
const latestTencentReport = {
|
||||
...olderTencentReport,
|
||||
meta: {
|
||||
...olderTencentReport.meta,
|
||||
id: 11,
|
||||
queryId: 'q-11',
|
||||
stockCode: '00700.HK',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [olderTencentItem],
|
||||
selectedReport: olderTencentReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList).mockResolvedValue({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestTencentItem, olderTencentItem],
|
||||
});
|
||||
vi.mocked(historyApi.getDetail).mockResolvedValue(latestTencentReport);
|
||||
|
||||
await useStockPoolStore.getState().refreshHistoryForCompletedTask(createTask({
|
||||
stockCode: '00700.HK',
|
||||
stockName: '腾讯控股',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}));
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(historyApi.getDetail).toHaveBeenCalledWith(11);
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([11, 10]);
|
||||
expect(state.selectedReport?.meta.id).toBe(11);
|
||||
});
|
||||
|
||||
it('does not replace the selected report when another stock task completes', async () => {
|
||||
const otherReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 3,
|
||||
queryId: 'q-3',
|
||||
stockCode: 'AAPL',
|
||||
stockName: 'Apple',
|
||||
},
|
||||
};
|
||||
const latestItem = {
|
||||
...historyItem,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [historyItem],
|
||||
selectedReport: otherReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList).mockResolvedValue({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
|
||||
await useStockPoolStore.getState().refreshHistoryForCompletedTask(createTask({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}));
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(historyApi.getDetail).not.toHaveBeenCalled();
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([2, 1]);
|
||||
expect(state.selectedReport?.meta.stockCode).toBe('AAPL');
|
||||
});
|
||||
|
||||
it('does not auto-switch to completed-task latest when the selected report changed before the refresh response returns', async () => {
|
||||
const latestItem = {
|
||||
...historyItem,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
const latestCompletedReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
},
|
||||
};
|
||||
const switchedReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 3,
|
||||
queryId: 'q-3',
|
||||
},
|
||||
};
|
||||
const completedRefreshResponse = createDeferred<HistoryListResponse>();
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [historyItem],
|
||||
selectedReport: historyReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList).mockReturnValue(completedRefreshResponse.promise);
|
||||
vi.mocked(historyApi.getDetail).mockResolvedValue(latestCompletedReport);
|
||||
|
||||
const completedRefreshPromise = useStockPoolStore.getState().refreshHistoryForCompletedTask(createTask({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}));
|
||||
|
||||
useStockPoolStore.setState({
|
||||
selectedReport: switchedReport,
|
||||
});
|
||||
|
||||
completedRefreshResponse.resolve({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
await completedRefreshPromise;
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([2, 1]);
|
||||
expect(state.selectedReport?.meta.id).toBe(3);
|
||||
expect(historyApi.getDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not auto-switch report when user selection is pending during completed-task refresh', async () => {
|
||||
const latestItem = {
|
||||
...historyItem,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
};
|
||||
const completedRefreshResponse = createDeferred<HistoryListResponse>();
|
||||
const userSelectionDetail = createDeferred<AnalysisReport>();
|
||||
const userSelectionReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 3,
|
||||
queryId: 'q-3',
|
||||
stockCode: 'AAPL',
|
||||
stockName: 'Apple',
|
||||
createdAt: '2026-03-18T07:00:00Z',
|
||||
},
|
||||
};
|
||||
const latestCompletedReport = {
|
||||
...historyReport,
|
||||
meta: {
|
||||
...historyReport.meta,
|
||||
id: 2,
|
||||
queryId: 'q-2',
|
||||
createdAt: '2026-03-18T09:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
useStockPoolStore.setState({
|
||||
historyItems: [
|
||||
historyItem,
|
||||
{
|
||||
...historyItem,
|
||||
id: 3,
|
||||
queryId: 'q-3',
|
||||
stockCode: 'AAPL',
|
||||
stockName: 'Apple',
|
||||
createdAt: '2026-03-18T07:00:00Z',
|
||||
},
|
||||
],
|
||||
selectedReport: historyReport,
|
||||
});
|
||||
vi.mocked(historyApi.getList).mockReturnValueOnce(completedRefreshResponse.promise);
|
||||
vi.mocked(historyApi.getDetail)
|
||||
.mockReturnValueOnce(userSelectionDetail.promise)
|
||||
.mockResolvedValue(latestCompletedReport);
|
||||
|
||||
const completedRefreshPromise = useStockPoolStore.getState().refreshHistoryForCompletedTask(
|
||||
createTask({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
}),
|
||||
);
|
||||
const manualSelectionPromise = useStockPoolStore.getState().selectHistoryItem(3);
|
||||
|
||||
completedRefreshResponse.resolve({
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
items: [latestItem, historyItem],
|
||||
});
|
||||
await completedRefreshPromise;
|
||||
|
||||
const midState = useStockPoolStore.getState();
|
||||
expect(midState.selectedReport?.meta.id).toBe(historyReport.meta.id);
|
||||
expect(historyApi.getDetail).toHaveBeenCalledWith(3);
|
||||
expect(historyApi.getDetail).toHaveBeenCalledTimes(1);
|
||||
|
||||
userSelectionDetail.resolve(userSelectionReport);
|
||||
await manualSelectionPromise;
|
||||
|
||||
const state = useStockPoolStore.getState();
|
||||
expect(state.selectedReport?.meta.id).toBe(3);
|
||||
expect(state.selectedReport?.meta.stockCode).toBe('AAPL');
|
||||
expect(state.historyItems.map((item) => item.id)).toEqual([2, 1, 3]);
|
||||
});
|
||||
|
||||
it('ignores late history responses after dashboard reset', async () => {
|
||||
const deferred = createDeferred<{
|
||||
total: number;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getParsedApiError } from '../api/error';
|
||||
import { historyApi } from '../api/history';
|
||||
import type { AnalysisReport, HistoryItem, HistoryListResponse, ReportLanguage, StockBarItem, StockHistoryFilters, StockHistoryRange, TaskInfo } from '../types/analysis';
|
||||
import { getRecentStartDate, getTodayInShanghai } from '../utils/format';
|
||||
import { normalizeStockCode } from '../utils/stockCode';
|
||||
import { isObviouslyInvalidStockQuery, looksLikeStockCode, validateStockCode } from '../utils/validation';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -18,6 +19,7 @@ type FetchHistoryOptions = {
|
||||
autoSelectFirst?: boolean;
|
||||
reset?: boolean;
|
||||
silent?: boolean;
|
||||
selectLatestForStockCode?: string;
|
||||
};
|
||||
|
||||
type SubmitAnalysisOptions = {
|
||||
@@ -31,6 +33,11 @@ type SubmitAnalysisOptions = {
|
||||
reportLanguage?: ReportLanguage;
|
||||
};
|
||||
|
||||
type CompletedTaskSelectionIntent = {
|
||||
manualSelectionSeq: number;
|
||||
selectedReportId: number | undefined;
|
||||
};
|
||||
|
||||
let reportRequestSeq = 0;
|
||||
let analyzeRequestSeq = 0;
|
||||
let historyRequestSeq = 0;
|
||||
@@ -38,7 +45,10 @@ let marketReviewHistoryRequestSeq = 0;
|
||||
let stockHistoryRequestSeq = 0;
|
||||
let activeTaskRequestSeq = 0;
|
||||
let activeTaskLocalRevision = 0;
|
||||
let manualSelectionRequestSeq = 0;
|
||||
let manualSelectionRequestId = 0;
|
||||
const dismissedTaskIds = new Set<string>();
|
||||
const pendingCompletedTaskSelectionKeys = new Map<string, CompletedTaskSelectionIntent>();
|
||||
|
||||
export interface StockPoolState {
|
||||
query: string;
|
||||
@@ -88,11 +98,12 @@ export interface StockPoolState {
|
||||
loadMoreStockHistory: () => Promise<void>;
|
||||
loadInitialHistory: () => Promise<void>;
|
||||
refreshHistory: (silent?: boolean) => Promise<void>;
|
||||
refreshHistoryForCompletedTask: (task: TaskInfo) => Promise<void>;
|
||||
loadMoreHistory: () => Promise<void>;
|
||||
loadMarketReviewHistory: () => Promise<void>;
|
||||
refreshMarketReviewHistory: (silent?: boolean) => Promise<void>;
|
||||
loadMoreMarketReviewHistory: () => Promise<void>;
|
||||
selectHistoryItem: (recordId: number) => Promise<void>;
|
||||
selectHistoryItem: (recordId: number, isUserInitiated?: boolean) => Promise<void>;
|
||||
toggleHistorySelection: (recordId: number) => void;
|
||||
toggleSelectAllVisible: () => void;
|
||||
deleteSelectedHistory: () => Promise<void>;
|
||||
@@ -238,6 +249,87 @@ function normalizeSelectedReport(report: AnalysisReport): AnalysisReport {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStockCodeKey(stockCode: string | undefined): string {
|
||||
const trimmed = (stockCode ?? '').trim();
|
||||
return trimmed ? normalizeStockCode(trimmed).toUpperCase() : '';
|
||||
}
|
||||
|
||||
function queueCompletedTaskSelection(
|
||||
stockCode: string | undefined,
|
||||
selectedReport: AnalysisReport | null,
|
||||
): void {
|
||||
const key = normalizeStockCodeKey(stockCode);
|
||||
if (key) {
|
||||
pendingCompletedTaskSelectionKeys.set(key, {
|
||||
manualSelectionSeq: manualSelectionRequestSeq,
|
||||
selectedReportId: selectedReport?.meta.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function consumeCompletedTaskSelection(items: HistoryItem[], selectedReport: AnalysisReport | null): HistoryItem | undefined {
|
||||
if (pendingCompletedTaskSelectionKeys.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (manualSelectionRequestId !== 0) {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (selectedReport?.meta.reportType === 'market_review') {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (selectedReport) {
|
||||
const selectedStockCode = normalizeStockCodeKey(selectedReport.meta.stockCode);
|
||||
const pendingSelectionIntent = selectedStockCode
|
||||
? pendingCompletedTaskSelectionKeys.get(selectedStockCode)
|
||||
: undefined;
|
||||
if (!selectedStockCode || pendingSelectionIntent === undefined) {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
return undefined;
|
||||
}
|
||||
if (pendingSelectionIntent.manualSelectionSeq !== manualSelectionRequestSeq) {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
return undefined;
|
||||
}
|
||||
if (pendingSelectionIntent.selectedReportId !== selectedReport.meta.id) {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const key of Array.from(pendingCompletedTaskSelectionKeys.keys())) {
|
||||
if (key !== selectedStockCode) {
|
||||
pendingCompletedTaskSelectionKeys.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const latestItem = items.find(
|
||||
(item) =>
|
||||
item.reportType !== 'market_review' &&
|
||||
normalizeStockCodeKey(item.stockCode) === selectedStockCode,
|
||||
);
|
||||
if (latestItem) {
|
||||
pendingCompletedTaskSelectionKeys.delete(selectedStockCode);
|
||||
}
|
||||
return latestItem;
|
||||
}
|
||||
|
||||
const latestItem = items.find((item) => {
|
||||
if (item.reportType === 'market_review') {
|
||||
return false;
|
||||
}
|
||||
const stockCode = normalizeStockCodeKey(item.stockCode);
|
||||
const pendingSelectionIntent = pendingCompletedTaskSelectionKeys.get(stockCode);
|
||||
return stockCode.length > 0 && pendingSelectionIntent?.manualSelectionSeq === manualSelectionRequestSeq;
|
||||
});
|
||||
if (latestItem) {
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
}
|
||||
return latestItem;
|
||||
}
|
||||
|
||||
function isDateInHistoryRange(createdAt: string | undefined, range: StockHistoryRange): boolean {
|
||||
if (range === 'all') {
|
||||
return true;
|
||||
@@ -353,9 +445,17 @@ async function fetchHistory(
|
||||
set: (partial: Partial<StockPoolState>) => void,
|
||||
options: FetchHistoryOptions = {},
|
||||
): Promise<HistoryListResponse | null> {
|
||||
const { autoSelectFirst = false, reset = true, silent = false } = options;
|
||||
const {
|
||||
autoSelectFirst = false,
|
||||
reset = true,
|
||||
silent = false,
|
||||
selectLatestForStockCode,
|
||||
} = options;
|
||||
const currentState = get();
|
||||
const page = reset ? 1 : currentState.currentPage + 1;
|
||||
if (reset) {
|
||||
queueCompletedTaskSelection(selectLatestForStockCode, currentState.selectedReport);
|
||||
}
|
||||
const requestId = ++historyRequestSeq;
|
||||
|
||||
if (!silent) {
|
||||
@@ -400,8 +500,14 @@ async function fetchHistory(
|
||||
selectedHistoryIds: get().selectedHistoryIds.filter((id) => visibleIds.has(id)),
|
||||
});
|
||||
|
||||
if (autoSelectFirst && response.items.length > 0 && !get().selectedReport) {
|
||||
await get().selectHistoryItem(response.items[0].id);
|
||||
if (reset) {
|
||||
const latestCompletedTaskItem = consumeCompletedTaskSelection(response.items, get().selectedReport);
|
||||
const selectedReport = get().selectedReport;
|
||||
if (latestCompletedTaskItem && latestCompletedTaskItem.id !== selectedReport?.meta.id) {
|
||||
await get().selectHistoryItem(latestCompletedTaskItem.id, false);
|
||||
} else if (autoSelectFirst && response.items.length > 0 && !selectedReport) {
|
||||
await get().selectHistoryItem(response.items[0].id, false);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -554,6 +660,14 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
await fetchHistory(get, set, { reset: true, silent });
|
||||
},
|
||||
|
||||
refreshHistoryForCompletedTask: async (task) => {
|
||||
await fetchHistory(get, set, {
|
||||
reset: true,
|
||||
silent: true,
|
||||
selectLatestForStockCode: task.reportType === 'market_review' ? undefined : task.stockCode,
|
||||
});
|
||||
},
|
||||
|
||||
loadMoreHistory: async () => {
|
||||
const state = get();
|
||||
if (state.isLoadingMore || !state.hasMore) {
|
||||
@@ -578,8 +692,12 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
await fetchMarketReviewHistory(get, set, { reset: false });
|
||||
},
|
||||
|
||||
selectHistoryItem: async (recordId) => {
|
||||
selectHistoryItem: async (recordId, isUserInitiated = true) => {
|
||||
const requestId = ++reportRequestSeq;
|
||||
if (isUserInitiated) {
|
||||
manualSelectionRequestSeq += 1;
|
||||
manualSelectionRequestId = requestId;
|
||||
}
|
||||
const shouldShowInitialLoading = !get().selectedReport;
|
||||
|
||||
if (shouldShowInitialLoading) {
|
||||
@@ -617,6 +735,10 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
error: getParsedApiError(error),
|
||||
isLoadingReport: false,
|
||||
});
|
||||
} finally {
|
||||
if (isUserInitiated && manualSelectionRequestId === requestId) {
|
||||
manualSelectionRequestId = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -666,7 +788,7 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
if (selectedWasDeleted) {
|
||||
const nextItem = freshPage?.items?.[0];
|
||||
if (nextItem) {
|
||||
await get().selectHistoryItem(nextItem.id);
|
||||
await get().selectHistoryItem(nextItem.id, false);
|
||||
} else {
|
||||
stockHistoryRequestSeq += 1;
|
||||
resetStockHistoryState(set);
|
||||
@@ -730,7 +852,7 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
if (selectedWasDeleted) {
|
||||
const nextItem = freshPage?.items?.[0];
|
||||
if (nextItem) {
|
||||
await get().selectHistoryItem(nextItem.id);
|
||||
await get().selectHistoryItem(nextItem.id, false);
|
||||
} else {
|
||||
set({ selectedReport: null });
|
||||
}
|
||||
@@ -914,9 +1036,12 @@ export const useStockPoolStore = create<StockPoolState>((set, get) => ({
|
||||
stockHistoryRequestSeq += 1;
|
||||
reportRequestSeq = 0;
|
||||
analyzeRequestSeq = 0;
|
||||
manualSelectionRequestSeq = 0;
|
||||
manualSelectionRequestId = 0;
|
||||
activeTaskRequestSeq += 1;
|
||||
activeTaskLocalRevision += 1;
|
||||
dismissedTaskIds.clear();
|
||||
pendingCompletedTaskSelectionKeys.clear();
|
||||
set({ ...initialState });
|
||||
},
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
- [修复] 修复通知 Markdown 表格转换在空单元格后将后续内容错配到错误表头的问题。
|
||||
- [修复] 将 Docker 可安装的 Longbridge SDK 版本固定为 0.2.75,避免 `longbridge>=0.2.77` 从包索引消失后导致 docker-build 失败。
|
||||
- [修复] Web 首页重新分析完成后自动切换到同一股票最新生成的报告,避免仍停留在旧报告内容。
|
||||
|
||||
- [修复] 默认通知报告补充展示 `dashboard.phase_decision` 盘中决策护栏字段,避免与模板渲染路径展示不一致。
|
||||
- [修复] 修复 Windows 环境下 Web/Desktop 静态 JS 资源可能被识别为 `text/plain` 导致前端黑屏的问题。
|
||||
|
||||
Reference in New Issue
Block a user