385 lines
13 KiB
TypeScript
385 lines
13 KiB
TypeScript
"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>
|
||
)
|
||
}
|