FA3-Datafetch/frontend/src/app/page.tsx

385 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { useSearchParams } from "next/navigation"
import { cn } from "@/lib/utils"
import { SearchStock } from "@/components/search-stock"
import { HistoryList } from "@/components/history-list"
import { DataSourceSelector } from "@/components/data-source-selector"
import { DataStatusCard } from "@/components/data-status-card"
import { FinancialTables } from "@/components/financial-tables"
import { AnalysisTrigger } from "@/components/analysis-trigger"
import { AnalysisReport } from "@/components/analysis-report"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ArrowLeft, Building2, RefreshCw, Loader2, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react"
import type { SearchResult } from "@/lib/types"
import { useFinancialData } from "@/hooks/use-financial-data"
import { Progress } from "@/components/ui/progress"
import { formatTimestamp } from "@/lib/formatters"
import { BloombergView } from "@/components/bloomberg-view"
import { HeaderPortal } from "@/components/header-portal"
import { AppSidebar } from "@/components/app-sidebar"
import { StockChart } from "@/components/stock-chart"
import { AiDiscussionView } from "@/components/ai-discussion-view"
import { HistoryView } from "@/components/history-view"
export default function Home() {
const searchParams = useSearchParams()
// 状态管理
const [selectedCompany, setSelectedCompany] = useState<SearchResult | null>(null)
const [selectedDataSource, setSelectedDataSource] = useState<string>("iFinD")
const [isSidebarOpen, setIsSidebarOpen] = useState(true)
const [companyId, setCompanyId] = useState<number | null>(null)
const [analysisId, setAnalysisId] = useState<number | null>(null)
const [oneTimeModel, setOneTimeModel] = useState<string | undefined>(undefined)
const [currency, setCurrency] = useState<string>("Auto")
// View State (home, financial, chart)
const [currentView, setCurrentView] = useState<string>("home")
// 处理公司选择
const handleCompanySelect = (company: SearchResult, dataSource?: string) => {
setSelectedCompany(company)
setCompanyId(null)
setAnalysisId(null)
setOneTimeModel(undefined)
// Switch to financial view by default when company is selected
setCurrentView("financial")
// 如果没有传入数据源,则根据市场设置默认值
const targetDataSource = dataSource || (company.market === 'CN' ? 'Tushare' : 'iFinD')
setSelectedDataSource(targetDataSource)
}
// 监听 URL 参数变化实现快速导航
useEffect(() => {
const symbol = searchParams.get('symbol')
const market = searchParams.get('market')
const source = searchParams.get('source')
const name = searchParams.get('name')
if (symbol && market && source) {
const company: SearchResult = {
symbol,
market,
company_name: name || symbol
}
handleCompanySelect(company, source)
}
}, [searchParams])
// 数据准备就绪
const handleDataReady = (id: number) => {
setCompanyId(id)
}
// AI分析完成
const handleAnalysisComplete = (id: number) => {
setAnalysisId(id)
}
// 返回搜索 (Reset)
const handleBackToSearch = () => {
setSelectedCompany(null)
setCompanyId(null)
setAnalysisId(null)
setOneTimeModel(undefined)
setCurrentView("home")
}
// Navigation Handler
const handleTabChange = (tab: string) => {
if (tab === "home") {
handleBackToSearch()
} else {
setCurrentView(tab)
}
}
// Render Content based on View
const renderContent = () => {
// History View (Global, no company selection needed)
if (currentView === "history") {
return (
<div className="h-full">
<HistoryView />
</div>
)
}
// Home View
if (!selectedCompany || currentView === "home") {
return (
<div className="max-w-7xl mx-auto flex flex-col gap-8 p-8">
<div className="flex flex-col gap-4">
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
AI驱动的分析
</p>
</div>
{/* 搜索组件 */}
<div className="grid grid-cols-1 gap-6">
<SearchStockWithSelection onSelect={handleCompanySelect} />
</div>
{/* 历史记录 */}
<div className="w-full">
<HistoryList onSelect={handleCompanySelect} />
</div>
</div>
)
}
// Chart View
if (currentView === "chart") {
return (
<div className="h-full p-4 flex flex-col gap-4">
{/* Top Navigation for Chart View */}
<div className="flex flex-row items-center justify-between px-2">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBackToSearch} className="gap-2">
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold">{selectedCompany.company_name}</h1>
<Badge variant="outline">{selectedCompany.symbol}</Badge>
<Badge>{selectedCompany.market}</Badge>
</div>
</div>
</div>
<StockChart symbol={selectedCompany.symbol} market={selectedCompany.market} />
</div>
)
}
// AI Discussion View
if (currentView === "ai-research") {
return (
<div className="h-full">
<AiDiscussionView
companyName={selectedCompany.company_name}
symbol={selectedCompany.symbol}
market={selectedCompany.market}
/>
</div>
)
}
// Financial Data View (Default fallback)
return (
<div className="w-full pl-2 pr-32 py-6 space-y-6">
{/* 顶部导航栏 (Portal Target) */}
<div className="grid grid-cols-1 gap-6">
{/* 数据源选择 */}
<div className="flex flex-col gap-4">
<div className="hidden">
<DataSourceSelector
market={selectedCompany.market}
selectedSource={selectedDataSource}
onSourceChange={setSelectedDataSource}
/>
</div>
{/* 数据获取状态 */}
<CompanyAnalysisView
company={selectedCompany}
dataSource={selectedDataSource}
onDataReady={handleDataReady}
onAnalysisComplete={handleAnalysisComplete}
selectedModel={oneTimeModel}
currency={currency}
setCurrency={setCurrency}
/>
</div>
</div>
</div>
)
}
return (
<div className="flex h-screen w-full bg-background overflow-hidden">
{/* Sidebar */}
<div
className={cn(
"transition-all duration-300 ease-in-out border-r bg-card flex-shrink-0 z-10 overflow-hidden",
isSidebarOpen ? "w-64" : "w-0 border-r-0"
)}
>
<AppSidebar
activeTab={currentView}
onTabChange={handleTabChange}
hasSelectedCompany={!!selectedCompany}
className="w-64 border-r-0"
/>
</div>
{/* Main Content Area Wrapper */}
<div className="flex-1 relative flex flex-col min-w-0 overflow-hidden bg-background/50">
{/* Sidebar Toggle Button */}
<Button
variant="secondary"
size="icon"
className="absolute left-0 top-1/2 -translate-y-1/2 z-20 h-12 w-4 p-0 rounded-l-none rounded-r-lg border border-l-0 shadow-sm hover:w-8 transition-all opacity-50 hover:opacity-100"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
title={isSidebarOpen ? "收起导航" : "展开导航"}
>
{isSidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
{/* Scrollable Content */}
<main className="flex-1 overflow-auto">
{renderContent()}
</main>
</div>
</div>
)
}
// ----------------------------------------------------------------------
// Sub-components
// ----------------------------------------------------------------------
function CompanyAnalysisView({
company,
dataSource,
onDataReady,
onAnalysisComplete,
selectedModel,
currency,
setCurrency
}: {
company: SearchResult
dataSource: string
onDataReady: (id: number) => void
onAnalysisComplete: (id: number) => void
selectedModel?: string
currency: string
setCurrency: (c: string) => void
}) {
const {
status,
loading,
fetching,
error,
fetchData,
checkStatus,
updateStatus
} = useFinancialData(company, dataSource)
const progress = updateStatus ? {
percentage: updateStatus.progress_percentage || 0,
message: updateStatus.progress_message || ""
} : null
useEffect(() => {
if (status && status.has_data && status.company_id) {
onDataReady(status.company_id)
}
}, [status, onDataReady])
return (
<div className="space-y-6">
{/* Header Controls */}
<HeaderPortal>
<div className="flex items-center gap-2 mr-4 border-r pr-4">
<span className="font-bold whitespace-nowrap">{company.company_name}</span>
<Badge variant="outline" className="font-mono">{company.symbol}</Badge>
<Badge className="text-xs">{company.market}</Badge>
</div>
<div className="flex items-center gap-2 mr-4">
<Button
variant="outline"
size="sm"
onClick={() => fetchData(true, currency)}
disabled={loading || fetching}
className="gap-2"
>
{(loading || fetching) ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
{(loading || fetching) ? "更新中..." : "更新数据"}
</Button>
</div>
<div className="flex items-center space-x-1 mr-6 border rounded-md p-1 bg-muted/20">
{["Auto", "CNY", "USD"].map((opt) => (
<button
key={opt}
onClick={() => setCurrency(opt)}
className={`
px-3 py-1 text-xs font-medium rounded-sm transition-all
${currency === opt
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"}
`}
>
{opt}
</button>
))}
</div>
{fetching && (
<div className="flex items-center gap-2 mr-4 min-w-[200px]">
<span className="text-xs text-muted-foreground whitespace-nowrap">
{progress?.message || "准备中..."}
</span>
<Progress value={progress?.percentage || 0} className="h-2 w-20" />
</div>
)}
{!fetching && !loading && <div className="text-xs text-muted-foreground flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-green-500" /> : {status?.last_update?.date ? formatTimestamp(status.last_update.date) : "无记录"}
</div>}
</HeaderPortal>
{/* DataStatusCard usage removed because it duplicates logic and caused prop mismatch.
Status is now handled by HeaderPortal controls.
*/}
{error && (
<div className="p-4 border border-destructive/50 bg-destructive/10 rounded-lg text-destructive flex items-center gap-2">
<Loader2 className="h-4 w-4" /> {/* Reuse icon or AlertCircle */}
<span>{error}</span>
</div>
)}
{dataSource === 'Bloomberg' ? (
status?.company_id && (
<BloombergView
selectedCurrency={currency}
userMarket={company.market}
companyId={status.company_id}
lastUpdate={status.last_update?.date}
/>
)
) : (
status?.company_id && (
<FinancialTables
companyId={status.company_id}
dataSource={dataSource}
/>
)
)}
</div>
)
}
function SearchStockWithSelection({ onSelect }: { onSelect: (c: SearchResult, s?: string) => void }) {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<SearchStock onCompanySelect={onSelect} />
</CardContent>
</Card>
)
}