- Dockerfile: 添加 Python 虚拟环境,修复端口匹配,跳过 SSL 验证下载 Portwarden - entrypoint.sh: 支持 http:// 地址(用于测试) - frontend/src/lib/api.ts: 添加 getReport 函数 - frontend/next.config.ts: 移除无效的 turbopack 配置 - frontend/src/app/page.tsx: 添加 Suspense 边界包裹 useSearchParams - frontend/src/components/nav-header.tsx: 添加 Suspense 边界包裹 useSearchParams - bastian/: 添加从 lyman.p12 提取的 mTLS 证书文件
165 lines
6.6 KiB
TypeScript
165 lines
6.6 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { MonitorPlay } from "lucide-react"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { HeaderSearch } from "@/components/header-search"
|
|
import { useEffect, useState, Suspense } from "react"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
|
|
type RecentCompany = {
|
|
market: string
|
|
symbol: string
|
|
company_name: string
|
|
last_update: string
|
|
}
|
|
|
|
function NavHeaderInner() {
|
|
const router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
|
|
const [dataSource, setDataSource] = useState("Bloomberg")
|
|
const [companies, setCompanies] = useState<RecentCompany[]>([])
|
|
|
|
// Sync state with URL params
|
|
useEffect(() => {
|
|
const sourceParam = searchParams.get('source')
|
|
if (sourceParam && sourceParam !== dataSource) {
|
|
setDataSource(sourceParam)
|
|
}
|
|
}, [searchParams])
|
|
|
|
const handleDataSourceChange = (newSource: string) => {
|
|
setDataSource(newSource)
|
|
|
|
// If viewing a company, update URL to trigger reload with new source
|
|
const currentSymbol = searchParams.get('symbol')
|
|
if (currentSymbol) {
|
|
const params = new URLSearchParams(searchParams.toString())
|
|
params.set('source', newSource)
|
|
router.push(`/?${params.toString()}`)
|
|
}
|
|
}
|
|
|
|
// Fetch recent companies
|
|
useEffect(() => {
|
|
const fetchRecent = async () => {
|
|
try {
|
|
// Use relative path since we are in same domain mainly
|
|
const res = await fetch(`/api/data/recent?data_source=${dataSource}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setCompanies(Array.isArray(data) ? data : [])
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch recent companies:", err)
|
|
}
|
|
}
|
|
fetchRecent()
|
|
}, [dataSource])
|
|
|
|
const handleCompanyChange = (value: string) => {
|
|
if (!value) return
|
|
const [symbol, market] = value.split(':')
|
|
const company = companies.find(c => c.symbol === symbol && c.market === market)
|
|
|
|
// Push URL params to trigger page state update
|
|
// We encode company name as well to display it nicely before full details load
|
|
const params = new URLSearchParams()
|
|
params.set('symbol', symbol)
|
|
params.set('market', market)
|
|
params.set('source', dataSource)
|
|
if (company) params.set('name', company.company_name)
|
|
params.set('t', Date.now().toString()) // Force update
|
|
|
|
router.push(`/?${params.toString()}`)
|
|
}
|
|
|
|
return (
|
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 z-[60] relative">
|
|
<div className="flex h-14 items-center gap-4 px-6 lg:h-[60px] w-full px-8"> {/* Adjusted constraint */}
|
|
<Link className="flex items-center gap-2 font-semibold min-w-[140px]" href="/">
|
|
<MonitorPlay className="h-6 w-6" />
|
|
<span className="">股票分析 AI</span>
|
|
</Link>
|
|
|
|
{/* 快速选择栏 */}
|
|
<div className="hidden md:flex items-center gap-3 ml-6 px-4 border-l h-8">
|
|
<span className="text-sm text-muted-foreground whitespace-nowrap font-medium">快速访问:</span>
|
|
|
|
<Select value={dataSource} onValueChange={handleDataSourceChange}>
|
|
<SelectTrigger className="w-[120px] h-8 text-xs font-medium">
|
|
<SelectValue placeholder="数据源" />
|
|
</SelectTrigger>
|
|
<SelectContent className="z-[70]">
|
|
<SelectItem value="Bloomberg">Bloomberg</SelectItem>
|
|
<SelectItem value="iFinD">iFinD</SelectItem>
|
|
<SelectItem value="Tushare">Tushare</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select onValueChange={handleCompanyChange}>
|
|
<SelectTrigger className="w-[120px] h-8 text-xs font-mono">
|
|
<SelectValue placeholder="代码..." />
|
|
</SelectTrigger>
|
|
<SelectContent className="z-[70]">
|
|
{companies.map((c) => (
|
|
<SelectItem key={`${c.symbol}:${c.market}`} value={`${c.symbol}:${c.market}`}>
|
|
<span className="flex items-center gap-2 w-full">
|
|
<span className="font-bold w-[50px] text-left">{c.symbol}</span>
|
|
<span className="truncate flex-1 text-left">{c.company_name}</span>
|
|
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
{companies.length === 0 && (
|
|
<div className="p-2 text-xs text-muted-foreground text-center">无记录</div>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="w-[160px] ml-4">
|
|
<HeaderSearch defaultDataSource={dataSource} />
|
|
</div>
|
|
|
|
{/* Portal Target for Dynamic Content */}
|
|
<div id="header-portal-target" className="flex-1 flex items-center justify-end px-4 gap-4 min-w-0" />
|
|
|
|
<nav className="flex items-center gap-4 sm:gap-6 border-l pl-6">
|
|
<Link className="text-sm font-medium hover:underline underline-offset-4" href="/">
|
|
主页
|
|
</Link>
|
|
<Link className="text-sm font-medium hover:underline underline-offset-4" href="/config">
|
|
配置
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
// Suspense wrapper for useSearchParams
|
|
export function NavHeader() {
|
|
return (
|
|
<Suspense fallback={
|
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 z-[60] relative">
|
|
<div className="flex h-14 items-center gap-4 px-6 lg:h-[60px] w-full px-8">
|
|
<Link className="flex items-center gap-2 font-semibold min-w-[140px]" href="/">
|
|
<MonitorPlay className="h-6 w-6" />
|
|
<span className="">股票分析 AI</span>
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
}>
|
|
<NavHeaderInner />
|
|
</Suspense>
|
|
)
|
|
}
|