"use client"; /** * 主页组件 - 上市公司基本面分析平台 * * 功能包括: * - 股票搜索和建议 * - 财务数据查询和展示 * - 表格行配置管理 * - 执行状态显示 * - 图表可视化 */ import { useState, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { StatusBar, useStatusBar } from "@/components/ui/status-bar"; import { createDefaultStepManager } from "@/lib/execution-step-manager"; import { RowSettingsPanel } from "@/components/ui/row-settings"; import { useRowConfig } from "@/hooks/use-row-config"; import { EnhancedTable } from "@/components/ui/enhanced-table"; import { Notification } from "@/components/ui/notification"; import { normalizeTsCode, flattenApiGroups, enhanceErrorMessage, isRetryableError, formatFinancialValue, getMetricUnit } from "@/lib/financial-utils"; import type { MarketType, ChartType, CompanyInfo, CompanySuggestion, RevenueDataPoint, FinancialMetricConfig, FinancialDataSeries, ExecutionStep, BatchFinancialDataResponse, FinancialConfigResponse, SearchApiResponse, BatchDataRequest } from "@/types"; import { ResponsiveContainer, BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, } from "recharts"; export default function Home() { // ============================================================================ // 基础状态管理 // ============================================================================ const [market, setMarket] = useState("cn"); const [query, setQuery] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [chartType, setChartType] = useState("bar"); // ============================================================================ // 数据状态管理 // ============================================================================ const [items, setItems] = useState([]); const [selected, setSelected] = useState(null); const [configItems, setConfigItems] = useState([]); const [metricSeries, setMetricSeries] = useState({}); const [selectedMetric, setSelectedMetric] = useState("revenue"); const [selectedMetricName, setSelectedMetricName] = useState("营业收入"); const [paramToGroup, setParamToGroup] = useState>({}); const [paramToApi, setParamToApi] = useState>({}); // ============================================================================ // 搜索相关状态 // ============================================================================ const [suggestions, setSuggestions] = useState([]); const [typingTimer, setTypingTimer] = useState(null); // ============================================================================ // 状态栏管理 // ============================================================================ const { statusBarState, showStatusBar, showSuccess, showError, hideStatusBar } = useStatusBar(); // ============================================================================ // 执行步骤管理 // ============================================================================ const [stepManager] = useState(() => { return createDefaultStepManager({ onStepStart: (step: ExecutionStep, index: number, total: number) => { showStatusBar(step, index, total); }, onStepComplete: (_step: ExecutionStep, index: number, total: number) => { // If there are more steps, update to next step if (index < total - 1) { // This will be handled by the next step start } else { // All steps completed showSuccess(); } }, onStepError: (_step: ExecutionStep, _index: number, _total: number, error: Error) => { // 判断错误是否可重试 const isRetryable = isRetryableError(error) || stepManager.canRetry(); showError(error.message, isRetryable); }, onComplete: () => { showSuccess(); }, onError: (error: Error) => { // 判断错误是否可重试 const isRetryable = isRetryableError(error) || stepManager.canRetry(); showError(error.message, isRetryable); } }); }); // ============================================================================ // 表格行配置管理 // ============================================================================ const [isRowSettingsPanelOpen, setIsRowSettingsPanelOpen] = useState(false); // Row configuration management - memoize to prevent infinite re-renders const rowIds = useMemo(() => configItems.map(item => item.tushareParam || '').filter(Boolean), [configItems] ); const { rowConfigs, customRows, updateRowConfig, saveStatus, clearSaveStatus, addCustomRow, deleteCustomRow, updateRowOrder } = useRowConfig(selected?.ts_code || null, rowIds); const rowDisplayTexts = useMemo(() => { const texts = configItems.reduce((acc, item) => { if (item.tushareParam) { acc[item.tushareParam] = item.displayText; } return acc; }, {} as Record); // 添加自定义行的显示文本 Object.entries(customRows).forEach(([rowId, customRow]) => { texts[rowId] = customRow.displayText; }); return texts; }, [configItems, customRows]); // ============================================================================ // 搜索建议功能 // ============================================================================ /** * 获取搜索建议 * @param text - 搜索文本 */ async function fetchSuggestions(text: string): Promise { if (market !== "cn") { setSuggestions([]); return; } const searchQuery = (text || "").trim(); if (!searchQuery) { setSuggestions([]); return; } try { const response = await fetch( `http://localhost:8000/api/search?query=${encodeURIComponent(searchQuery)}&limit=8` ); const data: SearchApiResponse = await response.json(); const suggestions = Array.isArray(data?.items) ? data.items : []; setSuggestions(suggestions); } catch (error) { console.warn('Failed to fetch suggestions:', error); setSuggestions([]); } } // ============================================================================ // 搜索处理功能 // ============================================================================ /** * 重试搜索的函数 */ const retrySearch = async (): Promise => { if (stepManager.canRetry()) { try { await stepManager.retry(); } catch { // 错误已经在stepManager中处理 } } else { // 如果不能重试,重新执行搜索 await handleSearch(); } }; /** * 处理搜索请求 */ async function handleSearch(): Promise { // 重置状态 setError(""); setItems([]); setMetricSeries({}); setConfigItems([]); setSelectedMetric("revenue"); // 验证市场支持 if (market !== "cn") { setError("目前仅支持 A 股查询,请选择\"中国\"市场。"); return; } // 获取并验证股票代码 const tsCode = selected?.ts_code || normalizeTsCode(query); if (!tsCode) { setError("请输入有效的 A 股股票代码(如 600519 / 000001 或带后缀 600519.SH)。"); return; } setLoading(true); try { // 创建搜索执行步骤 const searchStep: ExecutionStep = { id: 'fetch_financial_data', name: '正在读取财务数据', description: '从Tushare API获取公司财务指标数据', execute: async () => { await executeSearchStep(tsCode); } }; // 清空之前的步骤并添加新的搜索步骤 stepManager.clearSteps(); stepManager.addStep(searchStep); // 执行搜索步骤 await stepManager.execute(); } catch (error) { const errorMsg = enhanceErrorMessage(error); setError(errorMsg); // 错误处理已经在stepManager的回调中处理 } finally { setLoading(false); } } /** * 执行搜索步骤的具体逻辑 * @param tsCode - 股票代码 */ async function executeSearchStep(tsCode: string): Promise { // 1) 获取配置(tushare专用),解析 api_groups -> 扁平 items const configResponse = await fetch(`http://localhost:8000/api/financial-config`); const configData: FinancialConfigResponse = await configResponse.json(); const groups = configData?.api_groups || {}; const { items, groupMap, apiMap } = flattenApiGroups(groups); setConfigItems(items); setParamToGroup(groupMap); setParamToApi(apiMap); // 2) 批量请求年度序列(同API字段合并读取) const years = 10; const metrics = Array.from(new Set(["revenue", ...items.map(i => i.tushareParam)])); const batchRequest: BatchDataRequest = { ts_code: tsCode, years, metrics }; const batchResponse = await fetch( `http://localhost:8000/api/financialdata/${encodeURIComponent(market)}/batch`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batchRequest), } ); const batchData: BatchFinancialDataResponse = await batchResponse.json(); const seriesObj = batchData?.series || {}; // 处理数据系列 const processedSeries: FinancialDataSeries = {}; for (const metric of metrics) { const series = Array.isArray(seriesObj[metric]) ? seriesObj[metric] : []; processedSeries[metric] = [...series].sort((a, b) => Number(a.year) - Number(b.year)); } setMetricSeries(processedSeries); // 3) 设置选中公司与默认图表序列(收入) setSelected({ ts_code: batchData?.ts_code || tsCode, name: batchData?.name }); const revenueSeries = processedSeries["revenue"] || []; setItems(revenueSeries.map(d => ({ year: d.year, revenue: d.value }))); const revenueName = items.find(i => i.tushareParam === "revenue")?.displayText || "营业收入"; setSelectedMetricName(revenueName); if (revenueSeries.length === 0) { throw new Error("未查询到数据,请确认代码或稍后重试。"); } } // ============================================================================ // 渲染组件 // ============================================================================ return (
{/* StatusBar Component */} {/* Configuration Save Status Notification */} {saveStatus.status !== 'idle' && ( )} {/* Row Settings Panel */} setIsRowSettingsPanelOpen(false)} rowConfigs={rowConfigs} rowDisplayTexts={rowDisplayTexts} onConfigChange={updateRowConfig} onRowOrderChange={updateRowOrder} onDeleteCustomRow={deleteCustomRow} enableRowReordering={true} />

上市公司基本面分析

使用 Next.js + shadcn/ui 构建。你可以在此搜索公司、查看财报与关键指标。

{ const v = e.target.value; setQuery(v); setSelected(null); if (typingTimer) clearTimeout(typingTimer); const t = setTimeout(() => fetchSuggestions(v), 250); setTypingTimer(t); }} /> {/* 下拉建议 */} {suggestions.length > 0 && (
{suggestions.map((s, i) => (
{ setQuery(`${s.ts_code} ${s.name}`); setSelected({ ts_code: s.ts_code, name: s.name }); setSuggestions([]); }} > {s.name} {s.ts_code}
))}
)}
{error && {error}}
{items.length > 0 && (
近10年指标(A股) 数据来自 Tushare,{selected && selected.name ? `${selected.name} (${selected.ts_code})` : selected?.ts_code}
{/* 使用 Recharts 渲染图表(动态单位) */} {(() => { // 获取当前选中指标的信息 const currentMetricInfo = configItems.find(ci => (ci.tushareParam || "") === selectedMetric); const metricGroup = currentMetricInfo?.group; const metricApi = currentMetricInfo?.api; const metricUnit = getMetricUnit(metricGroup, metricApi, selectedMetric); // 构建完整的图例名称 const legendName = `${selectedMetricName}${metricUnit}`; // 根据指标类型确定数据缩放和单位 const shouldScaleToYi = ( metricGroup === "income" || metricGroup === "balancesheet" || metricGroup === "cashflow" || selectedMetric === "total_mv" ); const chartData = items.map((d) => { let scaledValue = typeof d.revenue === "number" ? d.revenue : 0; if (shouldScaleToYi) { // 对于财务报表数据,转换为亿元 if (selectedMetric === "total_mv") { // 市值从万元转为亿元 scaledValue = scaledValue / 1e4; } else { // 其他财务数据从元转为亿元 scaledValue = scaledValue / 1e8; } } return { year: d.year, metricValue: scaledValue, }; }); return (
{chartType === "bar" ? ( `${Math.round(v)}`} /> { const v = Number(value); const nf1 = new Intl.NumberFormat("zh-CN", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); const nf0 = new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }); if (shouldScaleToYi) { if (selectedMetric === "total_mv") { return [`${nf0.format(Math.round(v))} 亿元`, selectedMetricName]; } else { return [`${nf1.format(v)} 亿元`, selectedMetricName]; } } else { return [`${nf1.format(v)}`, selectedMetricName]; } }} /> ) : ( `${Math.round(v)}`} /> { const v = Number(value); const nf1 = new Intl.NumberFormat("zh-CN", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); const nf0 = new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }); if (shouldScaleToYi) { if (selectedMetric === "total_mv") { return [`${nf0.format(Math.round(v))} 亿元`, selectedMetricName]; } else { return [`${nf1.format(v)} 亿元`, selectedMetricName]; } } else { return [`${nf1.format(v)}`, selectedMetricName]; } }} /> )}
); })()} {/* 增强数据表格 */} {(() => { const series = metricSeries; const allYears = Object.values(series) .flat() .map(d => d.year); if (allYears.length === 0) return null; const yearsDesc = Array.from(new Set(allYears)).sort((a, b) => Number(b) - Number(a)); const columns = yearsDesc; function valueOf(m: string, year: string): number | null | undefined { const s = series[m] || []; const f = s.find(d => d.year === year); return f ? f.value : undefined; } function fmtCell(m: string, y: string): string { const v = valueOf(m, y); const group = paramToGroup[m] || ""; const api = paramToApi[m] || ""; return formatFinancialValue(v, group, api, m); } // 行点击切换图表数据源 function onRowClick(m: string) { setSelectedMetric(m); // 指标中文名传给图表 const rowInfo = configItems.find(ci => (ci.tushareParam || "") === m); setSelectedMetricName(rowInfo?.displayText || m); const s = series[m] || []; setItems(s.map(d => ({ year: d.year, revenue: d.value }))); } // 准备表格数据 const baseTableData = configItems.map((row, idx) => { const m = (row.tushareParam || "").trim(); const values: Record = {}; if (m) { columns.forEach(year => { values[year] = fmtCell(m, year); }); } else { columns.forEach(year => { values[year] = "-"; }); } return { id: m || `row_${idx}`, displayText: row.displayText + getMetricUnit(row.group, row.api, row.tushareParam), values, group: row.group, api: row.api, tushareParam: row.tushareParam, isCustomRow: false }; }); // 添加自定义行数据(仅分隔线) const customRowData = Object.entries(customRows) .filter(([, customRow]) => customRow.customRowType === 'separator') .map(([rowId, customRow]) => { const values: Record = {}; columns.forEach(year => { values[year] = "-"; // 分隔线不显示数据 }); return { id: rowId, displayText: customRow.displayText, values, isCustomRow: true, customRowType: customRow.customRowType }; }); // 合并基础数据和自定义行数据 const tableData = [...baseTableData, ...customRowData]; return ( setIsRowSettingsPanelOpen(true)} onAddCustomRow={addCustomRow} onDeleteCustomRow={deleteCustomRow} onRowOrderChange={updateRowOrder} enableAnimations={true} animationDuration={300} enableVirtualization={configItems.length > 50} maxVisibleRows={50} enableRowDragging={true} /> ); })()}
)}
); }