import { createFileRoute, Link } from "@tanstack/react-router"; import { useState, useEffect, useRef, useCallback } from "react"; import { collectionsApi, sharesApi } from "@/lib/api-client"; import { fetchStockQuote, fetchStockSearch, getStockBoard, type StockQuote, type StockSearchResult } from "@/lib/stock-api"; import { getUserId, updateLastAccessed } from "@/lib/user-id"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Search, Plus, Share2, Trash2, TrendingUp, Loader2, Flame, Network, BarChart3, BrainCircuit } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/")({ component: Index, }); interface StockCollection { id: number; name: string; description: string | null; created_at: string; user_id: string; last_accessed_at: string; } interface CollectionStock { id: number; collection_id: number; stock_code: string; stock_name: string; added_at: string; added_price: number | null; } function Index() { const [collections, setCollections] = useState([]); const [searchKeyword, setSearchKeyword] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [selectedStock, setSelectedStock] = useState(null); const [isLoadingQuote, setIsLoadingQuote] = useState(false); const [isCreating, setIsCreating] = useState(false); const [newCollectionName, setNewCollectionName] = useState(""); const [newCollectionDesc, setNewCollectionDesc] = useState(""); const [refreshKey, setRefreshKey] = useState(0); // 用于触发集合卡片刷新 const userId = getUserId(); const searchTimerRef = useRef | null>(null); const highlightIndexRef = useRef(-1); useEffect(() => { loadCollections(); }, []); const loadCollections = async () => { try { const data = await collectionsApi.list(userId); setCollections(data || []); } catch (err) { console.error("加载集合失败:", err); toast.error("加载股票集合失败"); } }; const searchStocks = (keyword: string) => { const trimmed = keyword.trim(); setSearchKeyword(keyword); // 清空已选股票 setSelectedStock(null); if (!trimmed) { setSearchResults([]); setIsSearching(false); return; } setIsSearching(true); highlightIndexRef.current = -1; // 防抖:300ms if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); } searchTimerRef.current = setTimeout(async () => { try { const results = await fetchStockSearch(trimmed); setSearchResults(results); } catch (err) { console.error("搜索失败:", err); setSearchResults([]); } finally { setIsSearching(false); } }, 300); }; // 选中搜索结果后获取实时行情 const selectStock = async (result: StockSearchResult) => { setSearchResults([]); setIsLoadingQuote(true); try { const quote = await fetchStockQuote(result.code); if (quote && quote.name) { setSelectedStock(quote); } else { toast.info("未找到该股票行情"); } } catch (err) { console.error("获取行情失败:", err); const msg = err instanceof Error ? err.message : "获取行情失败"; toast.error(msg); } finally { setIsLoadingQuote(false); } }; // 键盘导航:上下选择,回车确认 const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (searchResults.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); highlightIndexRef.current = Math.min( highlightIndexRef.current + 1, searchResults.length - 1 ); // 强制刷新以更新高亮 setSearchResults([...searchResults]); } else if (e.key === "ArrowUp") { e.preventDefault(); highlightIndexRef.current = Math.max(highlightIndexRef.current - 1, 0); setSearchResults([...searchResults]); } else if (e.key === "Enter") { e.preventDefault(); const idx = highlightIndexRef.current; if (idx >= 0 && idx < searchResults.length) { selectStock(searchResults[idx]); } else if (searchResults.length > 0) { selectStock(searchResults[0]); } } else if (e.key === "Escape") { setSearchResults([]); highlightIndexRef.current = -1; } }; const createCollection = async () => { if (!newCollectionName.trim()) { toast.error("请输入集合名称"); return; } try { await collectionsApi.create(newCollectionName.trim(), userId, newCollectionDesc.trim()); toast.success("创建成功"); setNewCollectionName(""); setNewCollectionDesc(""); setIsCreating(false); loadCollections(); } catch (err) { console.error("创建失败:", err); toast.error("创建集合失败"); } }; const addStockToCollection = async (collectionId: number, stock: StockQuote) => { try { await collectionsApi.addStock(collectionId, { stock_code: stock.code, stock_name: stock.name, added_price: parseFloat(stock.currentPrice.toFixed(2)), }); // 更新集合的最后访问时间 await collectionsApi.update(collectionId, { last_accessed_at: new Date().toISOString(), }); toast.success(`已添加 ${stock.name}(${stock.code}) @ ¥${stock.currentPrice.toFixed(2)}`); setSelectedStock(null); setSearchKeyword(""); setSearchResults([]); setRefreshKey(prev => prev + 1); // 触发集合卡片刷新 } catch (err) { console.error("添加失败:", err); const msg = err instanceof Error ? err.message : "添加股票失败"; toast.error(msg); } }; const deleteCollection = useCallback(async (collectionId: number) => { try { await collectionsApi.delete(collectionId); toast.success("删除成功"); loadCollections(); } catch (err) { console.error("删除失败:", err); toast.error("删除集合失败"); } }, []); return (

A股走势追踪

创建股票集合,分享历史走势

searchStocks(e.target.value)} onKeyDown={handleSearchKeyDown} className="pl-10" /> {isSearching && ( )} {/* 搜索结果下拉候选 */} {searchResults.length > 0 && !isSearching && (
{searchResults.map((result, idx) => { const board = getStockBoard(result.code); return ( ); })}
)} {/* 无结果提示 */} {searchKeyword.trim() && searchResults.length === 0 && !isSearching && !selectedStock && !isLoadingQuote && (
未找到匹配的股票
)}
创建股票集合
setNewCollectionName(e.target.value)} />