feat: AI分析页支持按日期查看历史报告(?date=,兼容?data=)
This commit is contained in:
Vendored
+2
-2
@@ -334,7 +334,7 @@ function renderReports(list) {
|
|||||||
document.getElementById('recentBody').innerHTML = list.map(r =>
|
document.getElementById('recentBody').innerHTML = list.map(r =>
|
||||||
`<tr>
|
`<tr>
|
||||||
<td>${r.trade_date}</td>
|
<td>${r.trade_date}</td>
|
||||||
<td><a href="/ai-analysis" target="_blank" style="color:var(--primary);text-decoration:none">${r.title || '-'}</a></td>
|
<td><a href="/ai-analysis?date=${r.trade_date}" target="_blank" style="color:var(--primary);text-decoration:none">${r.title || '-'}</a></td>
|
||||||
<td>${r.tokens_used || 0}</td>
|
<td>${r.tokens_used || 0}</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
).join('') || '<tr><td colspan="3" style="color:var(--muted-foreground);text-align:center">暂无数据</td></tr>';
|
).join('') || '<tr><td colspan="3" style="color:var(--muted-foreground);text-align:center">暂无数据</td></tr>';
|
||||||
@@ -349,7 +349,7 @@ function renderAllReports(list) {
|
|||||||
<td>${r.tokens_used || 0}</td>
|
<td>${r.tokens_used || 0}</td>
|
||||||
<td>${r.created_at || '-'}</td>
|
<td>${r.created_at || '-'}</td>
|
||||||
<td style="white-space:nowrap">
|
<td style="white-space:nowrap">
|
||||||
<a href="/ai-analysis" target="_blank" class="btn btn-ghost btn-sm" style="padding:3px 10px;font-size:11px;text-decoration:none">查看</a>
|
<a href="/ai-analysis?date=${r.trade_date}" target="_blank" class="btn btn-ghost btn-sm" style="padding:3px 10px;font-size:11px;text-decoration:none">查看</a>
|
||||||
<button class="btn btn-destructive btn-sm" style="padding:3px 10px;font-size:11px" onclick="deleteReport(${r.id})">删除</button>
|
<button class="btn btn-destructive btn-sm" style="padding:3px 10px;font-size:11px" onclick="deleteReport(${r.id})">删除</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
|
|||||||
@@ -77,6 +77,29 @@ async def list_reports():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ai-analysis/by-date/{trade_date}", summary="按交易日获取 AI 分析报告")
|
||||||
|
async def get_report_by_date(trade_date: str):
|
||||||
|
try:
|
||||||
|
datetime.strptime(trade_date, "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="日期格式错误,应为 YYYY-MM-DD")
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"""SELECT r.*,
|
||||||
|
(SELECT COUNT(*) FROM ai_reports WHERE trade_date <= r.trade_date) AS issue_number
|
||||||
|
FROM ai_reports r WHERE r.trade_date = ? AND r.report_type = 'daily' LIMIT 1""",
|
||||||
|
(trade_date,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail=f"{trade_date} 暂无分析报告")
|
||||||
|
item = dict_from_row(row)
|
||||||
|
item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]")
|
||||||
|
return JSONResponse({"data": item}, headers=_NO_CACHE_HEADERS)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ai-analysis/{report_id}", summary="AI 分析报告详情")
|
@router.get("/ai-analysis/{report_id}", summary="AI 分析报告详情")
|
||||||
async def get_report(report_id: int):
|
async def get_report(report_id: int):
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ export async function fetchAiReports(): Promise<AiReport[]> {
|
|||||||
return result.data || [];
|
return result.data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAiReportByDate(tradeDate: string): Promise<AiReport> {
|
||||||
|
const resp = await fetch(`${API_BASE}/api/ai-analysis/by-date/${tradeDate}`);
|
||||||
|
if (!resp.ok) throw new Error(`请求失败 (${resp.status})`);
|
||||||
|
const result = await resp.json();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAiReport(id: number): Promise<AiReport> {
|
export async function fetchAiReport(id: number): Promise<AiReport> {
|
||||||
const resp = await fetch(`${API_BASE}/api/ai-analysis/${id}`);
|
const resp = await fetch(`${API_BASE}/api/ai-analysis/${id}`);
|
||||||
if (!resp.ok) throw new Error(`请求失败 (${resp.status})`);
|
if (!resp.ok) throw new Error(`请求失败 (${resp.status})`);
|
||||||
|
|||||||
+63
-10
@@ -1,15 +1,22 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, Clock, Loader2, AlertCircle, Wrench, Calendar } from "lucide-react";
|
import { ArrowLeft, Clock, Loader2, AlertCircle, Wrench, Calendar } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import { Mermaid } from "../components/Mermaid";
|
import { Mermaid } from "../components/Mermaid";
|
||||||
import { fetchAiLatestReport } from "../lib/ai-analysis-api";
|
import { fetchAiLatestReport, fetchAiReportByDate, fetchAiReports } from "../lib/ai-analysis-api";
|
||||||
import { Card, CardContent } from "../components/ui/card";
|
import { Card, CardContent } from "../components/ui/card";
|
||||||
|
|
||||||
export const Route = createFileRoute("/ai-analysis")({
|
export const Route = createFileRoute("/ai-analysis")({
|
||||||
|
validateSearch: (search: Record<string, unknown>) => {
|
||||||
|
return {
|
||||||
|
date: typeof search.date === "string" ? search.date : undefined,
|
||||||
|
// 兼容 ?data=YYYY-MM-DD 写法
|
||||||
|
data: typeof search.data === "string" ? search.data : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
component: AiAnalysisPage,
|
component: AiAnalysisPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,12 +69,29 @@ function splitReport(content: string): { intro: string; sections: { title: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AiAnalysisPage() {
|
function AiAnalysisPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { date, data } = Route.useSearch();
|
||||||
|
const tradeDate = date ?? data ?? undefined;
|
||||||
|
const isHistorical = !!tradeDate;
|
||||||
|
|
||||||
const { data: report, isLoading, isError } = useQuery({
|
const { data: report, isLoading, isError } = useQuery({
|
||||||
queryKey: ["ai-report-latest"],
|
queryKey: isHistorical ? ["ai-report", tradeDate] : ["ai-report-latest"],
|
||||||
queryFn: fetchAiLatestReport,
|
queryFn: () => (isHistorical ? fetchAiReportByDate(tradeDate!) : fetchAiLatestReport()),
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: history = [] } = useQuery({
|
||||||
|
queryKey: ["ai-reports"],
|
||||||
|
queryFn: fetchAiReports,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const goDate = (d?: string) => {
|
||||||
|
navigate({ to: "/ai-analysis", search: d ? { date: d } : {} });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDate = tradeDate ?? report?.trade_date ?? "";
|
||||||
|
|
||||||
const mdComponents: Components = {
|
const mdComponents: Components = {
|
||||||
table({ children, ...props }) {
|
table({ children, ...props }) {
|
||||||
return (
|
return (
|
||||||
@@ -96,18 +120,35 @@ function AiAnalysisPage() {
|
|||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="max-w-4xl mx-auto px-3 sm:px-4 py-4 sm:py-6">
|
<div className="max-w-4xl mx-auto px-3 sm:px-4 py-4 sm:py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-4 sm:mb-6">
|
<div className="flex items-center justify-between gap-2 sm:gap-3 mb-4 sm:mb-6">
|
||||||
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
|
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
|
||||||
|
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors shrink-0">
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold flex items-center gap-2">
|
<h1 className="text-lg sm:text-xl md:text-2xl font-bold flex items-center gap-2 truncate">
|
||||||
<Calendar className="h-5 w-5 text-primary" />
|
<Calendar className="h-5 w-5 text-primary shrink-0" />
|
||||||
AI 收盘分析
|
AI 收盘分析
|
||||||
{report?.issue_number ? (
|
{report?.issue_number ? (
|
||||||
<span className="text-xs font-normal text-muted-foreground">总第 {report.issue_number} 期</span>
|
<span className="text-xs font-normal text-muted-foreground whitespace-nowrap">总第 {report.issue_number} 期</span>
|
||||||
) : null}
|
) : null}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{history.length > 0 && (
|
||||||
|
<select
|
||||||
|
value={history.some((h) => h.trade_date === selectedDate) ? selectedDate : ""}
|
||||||
|
onChange={(e) => goDate(e.target.value || undefined)}
|
||||||
|
className="text-xs sm:text-sm border border-border rounded-md bg-background px-2 py-1.5 text-foreground shrink-0"
|
||||||
|
aria-label="选择历史报告日期"
|
||||||
|
>
|
||||||
|
<option value="">最新</option>
|
||||||
|
{history.map((h) => (
|
||||||
|
<option key={h.id} value={h.trade_date}>
|
||||||
|
{h.trade_date}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -118,8 +159,20 @@ function AiAnalysisPage() {
|
|||||||
) : isError || !report ? (
|
) : isError || !report ? (
|
||||||
<div className="flex flex-col items-center gap-3 py-20">
|
<div className="flex flex-col items-center gap-3 py-20">
|
||||||
<AlertCircle className="h-6 w-6 text-destructive" />
|
<AlertCircle className="h-6 w-6 text-destructive" />
|
||||||
<p className="text-sm text-muted-foreground">暂无分析报告</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isHistorical ? `${tradeDate} 暂无分析报告` : "暂无分析报告"}
|
||||||
|
</p>
|
||||||
|
{isHistorical ? (
|
||||||
|
<Link
|
||||||
|
to="/ai-analysis"
|
||||||
|
search={{}}
|
||||||
|
className="text-xs text-primary hover:underline"
|
||||||
|
>
|
||||||
|
查看最新报告
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
<p className="text-xs text-muted-foreground/60">请通过管理面板触发 AI 分析</p>
|
<p className="text-xs text-muted-foreground/60">请通过管理面板触发 AI 分析</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user