Skip to content

Commit 8ce8d49

Browse files
Feat: 历史分析记录批量删除功能 (Batch Deletion) (ZhuLinsen#636)
* feat: implement batch deletion of analysis history with UI/UX improvements - Backend: Added DELETE /api/v1/history endpoint for bulk deletion by IDs. - Storage: Implemented DatabaseManager.delete_analysis_history_records with cascade cleanup of BacktestResult to prevent foreign key constraints. - Frontend: - Added checkbox-based multi-selection to HistoryList component. - Implemented batch delete logic in HomePage with automatic selection fallback. - Extracted and standardized ConfirmDialog component for consistent design across the app. - Updated ChatPage to use the new standardized ConfirmDialog. - Tests: Added unit tests for database cascade deletion and API endpoint functionality. * docs: update README and CHANGELOG for batch history deletion * feat: Implement the main HomePage for stock analysis and history management, supported by a new history service. --------- Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
1 parent 65034ec commit 8ce8d49

15 files changed

Lines changed: 599 additions & 164 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| 策略 | 市场策略系统 | 内置 A股「三段式复盘策略」与美股「Regime Strategy」,输出进攻/均衡/防守或 risk-on/neutral/risk-off 计划,并附“仅供参考,不构成投资建议”提示 |
3838
| 复盘 | 大盘复盘 | 每日市场概览、板块涨跌;支持 cn(A股)/us(美股)/both(两者) 切换 |
3939
| 智能导入 | 多源导入 | 支持图片、CSV/Excel 文件、剪贴板粘贴;Vision LLM 提取代码+名称;置信度分层确认;名称→代码解析(本地+拼音+AkShare) |
40+
| 历史记录 | 批量管理 | 支持多选、全选及批量删除历史分析记录,优化管理效率与 UI/UX 体验 |
4041
| 回测 | AI 回测验证 | 自动评估历史分析准确率,方向胜率、止盈止损命中率 |
4142
| **Agent 问股** | **策略对话** | **多轮策略问答,支持均线金叉/缠论/波浪等 11 种内置策略,Web/Bot/API 全链路** |
4243
| 推送 | 多渠道通知 | 企业微信、飞书、Telegram、钉钉、邮件、Pushover |

api/v1/endpoints/history.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import logging
1313
from typing import Optional
1414

15-
from fastapi import APIRouter, HTTPException, Query, Depends
15+
from fastapi import APIRouter, HTTPException, Query, Depends, Body
1616

1717
from api.deps import get_database_manager
1818
from api.v1.schemas.history import (
1919
HistoryListResponse,
2020
HistoryItem,
21+
DeleteHistoryRequest,
22+
DeleteHistoryResponse,
2123
NewsIntelItem,
2224
NewsIntelResponse,
2325
AnalysisReport,
@@ -116,6 +118,51 @@ def get_history_list(
116118
)
117119

118120

121+
@router.delete(
122+
"",
123+
response_model=DeleteHistoryResponse,
124+
responses={
125+
200: {"description": "删除成功"},
126+
400: {"description": "请求参数错误", "model": ErrorResponse},
127+
500: {"description": "服务器错误", "model": ErrorResponse},
128+
},
129+
summary="删除历史分析记录",
130+
description="按历史记录主键 ID 批量删除分析历史"
131+
)
132+
def delete_history_records(
133+
request: DeleteHistoryRequest = Body(...),
134+
db_manager: DatabaseManager = Depends(get_database_manager)
135+
) -> DeleteHistoryResponse:
136+
"""
137+
按主键 ID 批量删除历史分析记录。
138+
"""
139+
record_ids = sorted({record_id for record_id in request.record_ids if record_id is not None})
140+
if not record_ids:
141+
raise HTTPException(
142+
status_code=400,
143+
detail={
144+
"error": "invalid_request",
145+
"message": "record_ids 不能为空"
146+
}
147+
)
148+
149+
try:
150+
service = HistoryService(db_manager)
151+
deleted = service.delete_history_records(record_ids)
152+
return DeleteHistoryResponse(deleted=deleted)
153+
except HTTPException:
154+
raise
155+
except Exception as e:
156+
logger.error(f"删除历史记录失败: {e}", exc_info=True)
157+
raise HTTPException(
158+
status_code=500,
159+
detail={
160+
"error": "internal_error",
161+
"message": f"删除历史记录失败: {str(e)}"
162+
}
163+
)
164+
165+
119166
@router.get(
120167
"/{record_id}",
121168
response_model=AnalysisReport,

api/v1/schemas/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from api.v1.schemas.history import (
2424
HistoryItem,
2525
HistoryListResponse,
26+
DeleteHistoryRequest,
27+
DeleteHistoryResponse,
2628
NewsIntelItem,
2729
NewsIntelResponse,
2830
AnalysisReport,
@@ -75,6 +77,8 @@
7577
# history
7678
"HistoryItem",
7779
"HistoryListResponse",
80+
"DeleteHistoryRequest",
81+
"DeleteHistoryResponse",
7882
"NewsIntelItem",
7983
"NewsIntelResponse",
8084
"AnalysisReport",

api/v1/schemas/history.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ class Config:
6565
}
6666

6767

68+
class DeleteHistoryRequest(BaseModel):
69+
"""删除历史记录请求"""
70+
71+
record_ids: List[int] = Field(default_factory=list, description="要删除的历史记录主键 ID 列表")
72+
73+
74+
class DeleteHistoryResponse(BaseModel):
75+
"""删除历史记录响应"""
76+
77+
deleted: int = Field(..., description="实际删除的历史记录数量")
78+
79+
6880
class NewsIntelItem(BaseModel):
6981
"""新闻情报条目"""
7082

apps/dsa-web/package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/dsa-web/src/api/history.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,16 @@ export const historyApi = {
7777
const response = await apiClient.get<{ content: string }>(`/api/v1/history/${recordId}/markdown`);
7878
return response.data.content;
7979
},
80+
81+
/**
82+
* 批量删除历史记录
83+
* @param recordIds 分析历史记录主键 ID 列表
84+
*/
85+
deleteRecords: async (recordIds: number[]): Promise<{ deleted: number }> => {
86+
const response = await apiClient.delete<Record<string, unknown>>('/api/v1/history', {
87+
data: { record_ids: recordIds },
88+
});
89+
90+
return toCamelCase<{ deleted: number }>(response.data);
91+
},
8092
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type React from 'react';
2+
3+
interface ConfirmDialogProps {
4+
isOpen: boolean;
5+
title: string;
6+
message: string;
7+
confirmText?: string;
8+
cancelText?: string;
9+
isDanger?: boolean;
10+
onConfirm: () => void;
11+
onCancel: () => void;
12+
}
13+
14+
/**
15+
* Generic confirmation dialog component.
16+
* Style is consistent with ChatPage.
17+
*/
18+
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
19+
isOpen,
20+
title,
21+
message,
22+
confirmText = '确定',
23+
cancelText = '取消',
24+
isDanger = false,
25+
onConfirm,
26+
onCancel,
27+
}) => {
28+
if (!isOpen) return null;
29+
30+
return (
31+
<div
32+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-all"
33+
onClick={onCancel}
34+
>
35+
<div
36+
className="bg-elevated border border-white/10 rounded-xl p-6 max-w-sm w-full mx-4 shadow-2xl animate-in fade-in zoom-in duration-200"
37+
onClick={(e) => e.stopPropagation()}
38+
>
39+
<h3 className="text-white font-medium mb-2 text-lg">{title}</h3>
40+
<p className="text-sm text-secondary mb-6 leading-relaxed">
41+
{message}
42+
</p>
43+
<div className="flex justify-end gap-3">
44+
<button
45+
onClick={onCancel}
46+
className="px-4 py-2 rounded-lg text-sm font-medium text-secondary hover:text-white hover:bg-white/5 border border-white/10 transition-colors"
47+
>
48+
{cancelText}
49+
</button>
50+
<button
51+
onClick={onConfirm}
52+
className={`px-4 py-2 rounded-lg text-sm font-medium text-white transition-colors ${
53+
isDanger
54+
? 'bg-red-500/80 hover:bg-red-500 shadow-lg shadow-red-500/20'
55+
: 'bg-cyan/80 hover:bg-cyan shadow-lg shadow-cyan/20'
56+
}`}
57+
>
58+
{confirmText}
59+
</button>
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
};

apps/dsa-web/src/components/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './JsonViewer';
1010
export * from './Select';
1111
export * from './Badge';
1212
export * from './Pagination';
13+
export * from './ConfirmDialog';

0 commit comments

Comments
 (0)