FA3-Datafetch/frontend/src/components/nav-header.tsx
xucheng 77a08f1c55 修复 Docker 配置并添加 mTLS 证书支持
- 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 证书文件
2026-01-14 11:12:24 +08:00

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>
)
}