209 lines
7.8 KiB
TypeScript
209 lines
7.8 KiB
TypeScript
'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>
|
||
);
|
||
}
|