Fundamental_Analysis/frontend/src/app/config/page.tsx

209 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { useConfig, testConfig, useDataSourcesConfig, updateDataSourcesConfig } from '@/hooks/useApi';
import { useConfigStore } from '@/stores/useConfigStore';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import type { DataSourcesConfig, DataSourceProvider } from '@/types';
import { AIConfigTab } from './components/AIConfigTab';
import { DataSourcesConfigTab } from './components/DataSourcesConfigTab';
import { AnalysisConfigTab } from './components/AnalysisConfigTab';
import { SystemConfigTab } from './components/SystemConfigTab';
const defaultUrls: Partial<Record<DataSourceProvider, string>> = {
tushare: 'http://api.tushare.pro',
finnhub: 'https://finnhub.io/api/v1',
alphavantage: 'https://mcp.alphavantage.co/mcp',
};
export default function ConfigPage() {
// 从 Zustand store 获取全局状态
const { config, loading, error, setConfig } = useConfigStore();
// 使用 SWR hook 加载初始配置
useConfig();
// 本地表单状态
// 数据源本地状态
const { data: initialDataSources, error: dsError, isLoading: dsLoading, mutate: mutateDataSources } = useDataSourcesConfig();
const [localDataSources, setLocalDataSources] = useState<DataSourcesConfig>({});
// 测试结果状态
const [testResults, setTestResults] = useState<Record<string, { success: boolean; summary: string; details?: string } | null>>({});
// 保存状态
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState('');
// Shared state for AI Tab (needed for System Tab's import feature)
const [newProviderBaseUrl, setNewProviderBaseUrl] = useState('');
useEffect(() => {
if (initialDataSources) {
setLocalDataSources(initialDataSources);
}
}, [initialDataSources]);
const handleSave = async () => {
setSaving(true);
setSaveMessage('保存中...');
try {
if (initialDataSources) {
// Create a deep copy to avoid mutating the local state directly
const finalDataSources = JSON.parse(JSON.stringify(localDataSources));
for (const key in finalDataSources) {
const providerKey = key as DataSourceProvider;
const source = finalDataSources[providerKey];
// If the URL is empty or null, and there is a default URL, use it
if (source && (source.api_url === null || source.api_url.trim() === '') && defaultUrls[providerKey]) {
source.api_url = defaultUrls[providerKey];
}
}
await updateDataSourcesConfig(finalDataSources);
// After saving, mutate the local SWR cache with the final data
// and also update the component's local state to reflect the change.
await mutateDataSources(finalDataSources, false);
setLocalDataSources(finalDataSources);
}
setSaveMessage('保存成功!');
} catch (e: any) {
setSaveMessage(`保存失败: ${e.message}`);
} finally {
setSaving(false);
setTimeout(() => setSaveMessage(''), 5000);
}
};
const handleTest = async (type: string, data: any) => {
try {
const result = await testConfig(type, data);
const success = !!result?.success;
const summary = typeof result?.message === 'string' && result.message.trim().length > 0
? result.message
: (success ? '测试成功' : '测试失败');
setTestResults(prev => ({ ...prev, [type]: { success, summary } }));
} catch (e: any) {
// 结构化错误对象:{ summary, details? }
const summary: string = (e && typeof e === 'object' && 'summary' in e) ? String(e.summary) : (e?.message || '未知错误');
const details: string | undefined = (e && typeof e === 'object' && 'details' in e) ? (e.details ? String(e.details) : undefined) : undefined;
setTestResults(prev => ({
...prev,
[type]: { success: false, summary, details }
}));
}
};
const handleTestTushare = () => {
const cfg = localDataSources['tushare'];
handleTest('tushare', { api_key: cfg?.api_key, api_url: cfg?.api_url, enabled: cfg?.enabled });
};
const handleTestFinnhub = () => {
const cfg = localDataSources['finnhub'];
handleTest('finnhub', { api_key: cfg?.api_key, api_url: cfg?.api_url, enabled: cfg?.enabled });
};
const handleTestAlphaVantage = () => {
const cfg = localDataSources['alphavantage'];
handleTest('alphavantage', { api_key: cfg?.api_key, api_url: cfg?.api_url, enabled: cfg?.enabled });
};
const handleReset = () => {
if (initialDataSources) setLocalDataSources(initialDataSources);
setTestResults({});
setSaveMessage('');
};
if (loading) return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div>
<p className="mt-2 text-sm text-muted-foreground">...</p>
</div>
</div>
);
if (error) return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="text-red-500 text-lg mb-2"></div>
<p className="text-red-600">: {error}</p>
</div>
</div>
);
return (
<div className="container mx-auto py-6 space-y-6">
<header className="space-y-2">
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground">
AI
</p>
</header>
<Tabs defaultValue="ai" className="space-y-6">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="ai">AI服务</TabsTrigger>
<TabsTrigger value="data-sources"></TabsTrigger>
<TabsTrigger value="analysis"></TabsTrigger>
<TabsTrigger value="system"></TabsTrigger>
</TabsList>
<TabsContent value="ai" className="space-y-4">
<AIConfigTab
newProviderBaseUrl={newProviderBaseUrl}
setNewProviderBaseUrl={setNewProviderBaseUrl}
/>
</TabsContent>
<TabsContent value="data-sources" className="space-y-4">
<DataSourcesConfigTab
dsLoading={dsLoading}
dsError={dsError}
localDataSources={localDataSources}
setLocalDataSources={setLocalDataSources}
testResults={testResults}
handleTestTushare={handleTestTushare}
handleTestFinnhub={handleTestFinnhub}
handleTestAlphaVantage={handleTestAlphaVantage}
/>
</TabsContent>
<TabsContent value="analysis" className="space-y-4">
<AnalysisConfigTab />
</TabsContent>
<TabsContent value="system" className="space-y-4">
<SystemConfigTab
config={config}
setSaveMessage={setSaveMessage}
setNewProviderBaseUrl={setNewProviderBaseUrl}
/>
</TabsContent>
</Tabs>
<div className="flex items-center justify-between pt-6 border-t">
<div className="flex items-center gap-4">
<Button onClick={handleSave} disabled={saving} size="lg">
{saving ? '保存中...' : '保存所有配置'}
</Button>
<Button onClick={handleReset} variant="outline" size="lg">
</Button>
{saveMessage && (
<span className={`text-sm ${saveMessage.includes('成功') ? 'text-green-600' : 'text-red-600'}`}>
{saveMessage}
</span>
)}
</div>
<div className="text-sm text-muted-foreground">
: {new Date().toLocaleString()}
</div>
</div>
</div>
);
}