|
|
|
@ -20,35 +20,21 @@ import {
|
|
|
|
} from 'lucide-react';
|
|
|
|
} from 'lucide-react';
|
|
|
|
import { adminApi, type DataSourceConfig as DataSourceConfigType } from '@/services/adminApi';
|
|
|
|
import { adminApi, type DataSourceConfig as DataSourceConfigType } from '@/services/adminApi';
|
|
|
|
|
|
|
|
|
|
|
|
interface AKShareStatus {
|
|
|
|
const MARKET_DATA_SERVICE_ID = 'marketDataService';
|
|
|
|
connected: boolean;
|
|
|
|
|
|
|
|
version?: string;
|
|
|
|
|
|
|
|
supportedApis?: string[];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface AKShareConfig {
|
|
|
|
|
|
|
|
baseUrl: string;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default function DataSourceConfig() {
|
|
|
|
export default function DataSourceConfig() {
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const [sources, setSources] = useState<DataSourceConfigType[]>([]);
|
|
|
|
const [sources, setSources] = useState<DataSourceConfigType[]>([]);
|
|
|
|
const [akshareStatus, setAkshareStatus] = useState<AKShareStatus | null>(null);
|
|
|
|
|
|
|
|
const [akshareConfig, setAkshareConfig] = useState<AKShareConfig>({ baseUrl: 'http://localhost:8000' });
|
|
|
|
|
|
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
|
|
const [isEditingUrl, setIsEditingUrl] = useState(false);
|
|
|
|
|
|
|
|
const [editingUrl, setEditingUrl] = useState('');
|
|
|
|
|
|
|
|
const [savingUrl, setSavingUrl] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义数据源编辑状态
|
|
|
|
// 统一数据服务编辑状态
|
|
|
|
const [editingCustom, setEditingCustom] = useState(false);
|
|
|
|
const [editingService, setEditingService] = useState(false);
|
|
|
|
const [customUrl, setCustomUrl] = useState('http://localhost:8000');
|
|
|
|
const [serviceUrl, setServiceUrl] = useState('http://localhost:8080');
|
|
|
|
const [customEnabled, setCustomEnabled] = useState(false);
|
|
|
|
const [serviceEnabled, setServiceEnabled] = useState(false);
|
|
|
|
const [savingCustom, setSavingCustom] = useState(false);
|
|
|
|
const [savingService, setSavingService] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [testing, setTesting] = useState<string | null>(null);
|
|
|
|
const [testing, setTesting] = useState<string | null>(null);
|
|
|
|
const [syncing, setSyncing] = useState(false);
|
|
|
|
|
|
|
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
|
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查登录状态
|
|
|
|
// 检查登录状态
|
|
|
|
@ -68,54 +54,30 @@ export default function DataSourceConfig() {
|
|
|
|
const fetchData = async () => {
|
|
|
|
const fetchData = async () => {
|
|
|
|
setLoading(true);
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const [sourcesData, statusData, configData] = await Promise.allSettled([
|
|
|
|
const sourcesData = await adminApi.getDataSources();
|
|
|
|
adminApi.getDataSources(),
|
|
|
|
setSources(sourcesData);
|
|
|
|
adminApi.getAKShareStatus().catch(() => ({ connected: false })),
|
|
|
|
|
|
|
|
adminApi.getAKShareConfig().catch(() => ({ baseUrl: 'http://localhost:8000' })),
|
|
|
|
// 初始化统一数据服务状态
|
|
|
|
]);
|
|
|
|
const serviceSource = sourcesData.find(s => s.id === MARKET_DATA_SERVICE_ID || s.type === 'custom');
|
|
|
|
|
|
|
|
if (serviceSource) {
|
|
|
|
if (sourcesData.status === 'fulfilled') {
|
|
|
|
setServiceUrl(serviceSource.url);
|
|
|
|
setSources(sourcesData.value);
|
|
|
|
setServiceEnabled(serviceSource.enabled);
|
|
|
|
// 初始化自定义数据源状态
|
|
|
|
|
|
|
|
const customSource = sourcesData.value.find(s => s.id === 'custom');
|
|
|
|
|
|
|
|
if (customSource) {
|
|
|
|
|
|
|
|
setCustomUrl(customSource.url);
|
|
|
|
|
|
|
|
setCustomEnabled(customSource.enabled);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (sourcesData.reason?.message?.includes('登录')) {
|
|
|
|
|
|
|
|
setIsLoggedIn(false);
|
|
|
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
|
|
|
localStorage.removeItem('user');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (statusData.status === 'fulfilled') {
|
|
|
|
|
|
|
|
setAkshareStatus(statusData.value);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (configData.status === 'fulfilled') {
|
|
|
|
|
|
|
|
setAkshareConfig(configData.value);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('Failed to fetch data:', error);
|
|
|
|
console.error('Failed to fetch data:', error);
|
|
|
|
if (error.message?.includes('登录')) {
|
|
|
|
if (error.message?.includes('登录')) {
|
|
|
|
setIsLoggedIn(false);
|
|
|
|
setIsLoggedIn(false);
|
|
|
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
|
|
|
localStorage.removeItem('user');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 使用默认配置
|
|
|
|
setSources([
|
|
|
|
setSources([
|
|
|
|
{
|
|
|
|
{
|
|
|
|
id: 'akshare',
|
|
|
|
id: MARKET_DATA_SERVICE_ID,
|
|
|
|
name: 'AKShare 官方',
|
|
|
|
name: '统一数据服务',
|
|
|
|
type: 'akshare',
|
|
|
|
|
|
|
|
url: akshareConfig.baseUrl,
|
|
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
|
|
syncInterval: 5,
|
|
|
|
|
|
|
|
status: 'disconnected',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: 'custom',
|
|
|
|
|
|
|
|
name: '自定义数据源',
|
|
|
|
|
|
|
|
type: 'custom',
|
|
|
|
type: 'custom',
|
|
|
|
url: 'http://localhost:8080',
|
|
|
|
url: 'http://localhost:8080',
|
|
|
|
enabled: false,
|
|
|
|
enabled: true,
|
|
|
|
syncInterval: 5,
|
|
|
|
syncInterval: 5,
|
|
|
|
status: 'disconnected',
|
|
|
|
status: 'disconnected',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -125,7 +87,7 @@ export default function DataSourceConfig() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTestConnection = async (sourceId: string = 'akshare') => {
|
|
|
|
const handleTestConnection = async (sourceId: string = MARKET_DATA_SERVICE_ID) => {
|
|
|
|
setTesting(sourceId);
|
|
|
|
setTesting(sourceId);
|
|
|
|
setMessage(null);
|
|
|
|
setMessage(null);
|
|
|
|
|
|
|
|
|
|
|
|
@ -133,15 +95,9 @@ export default function DataSourceConfig() {
|
|
|
|
const result = await adminApi.testDataSource(sourceId);
|
|
|
|
const result = await adminApi.testDataSource(sourceId);
|
|
|
|
setMessage({
|
|
|
|
setMessage({
|
|
|
|
type: result.success ? 'success' : 'error',
|
|
|
|
type: result.success ? 'success' : 'error',
|
|
|
|
text: `[${sourceId === 'custom' ? '自定义数据源' : 'AKShare'}] ${result.message}`,
|
|
|
|
text: `[统一数据服务] ${result.message}`,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是AKShare,刷新状态
|
|
|
|
|
|
|
|
if (sourceId === 'akshare') {
|
|
|
|
|
|
|
|
const status = await adminApi.getAKShareStatus();
|
|
|
|
|
|
|
|
setAkshareStatus(status);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新数据源列表
|
|
|
|
// 刷新数据源列表
|
|
|
|
await fetchData();
|
|
|
|
await fetchData();
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
@ -154,20 +110,6 @@ export default function DataSourceConfig() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleManualSync = async () => {
|
|
|
|
|
|
|
|
setSyncing(true);
|
|
|
|
|
|
|
|
setMessage(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const result = await adminApi.triggerSync('akshare');
|
|
|
|
|
|
|
|
setMessage({ type: 'success', text: `同步任务已启动,任务ID: ${result.taskId}` });
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
setMessage({ type: 'error', text: error.message || '同步失败' });
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
setSyncing(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleToggleSource = async (sourceId: string) => {
|
|
|
|
const handleToggleSource = async (sourceId: string) => {
|
|
|
|
const source = sources.find(s => s.id === sourceId);
|
|
|
|
const source = sources.find(s => s.id === sourceId);
|
|
|
|
if (!source) return;
|
|
|
|
if (!source) return;
|
|
|
|
@ -179,86 +121,59 @@ export default function DataSourceConfig() {
|
|
|
|
setSources(prev => prev.map(s =>
|
|
|
|
setSources(prev => prev.map(s =>
|
|
|
|
s.id === sourceId ? { ...s, enabled: newEnabled } : s
|
|
|
|
s.id === sourceId ? { ...s, enabled: newEnabled } : s
|
|
|
|
));
|
|
|
|
));
|
|
|
|
setMessage({ type: 'success', text: `${source.name} 已${newEnabled ? '启用' : '禁用'}` });
|
|
|
|
setCustomEnabled(newEnabled);
|
|
|
|
|
|
|
|
setServiceEnabled(newEnabled);
|
|
|
|
|
|
|
|
setMessage({ type: 'success', text: `统一数据服务已${newEnabled ? '启用' : '禁用'}` });
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
setMessage({ type: 'error', text: error.message || '操作失败' });
|
|
|
|
setMessage({ type: 'error', text: error.message || '操作失败' });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 开始编辑 URL
|
|
|
|
// 开始编辑统一数据服务
|
|
|
|
const handleStartEditUrl = () => {
|
|
|
|
const handleStartEditService = () => {
|
|
|
|
setEditingUrl(akshareConfig.baseUrl);
|
|
|
|
const serviceSource = sources.find(s => s.id === MARKET_DATA_SERVICE_ID || s.type === 'custom');
|
|
|
|
setIsEditingUrl(true);
|
|
|
|
if (serviceSource) {
|
|
|
|
};
|
|
|
|
setServiceUrl(serviceSource.url);
|
|
|
|
|
|
|
|
setServiceEnabled(serviceSource.enabled);
|
|
|
|
// 取消编辑 URL
|
|
|
|
|
|
|
|
const handleCancelEditUrl = () => {
|
|
|
|
|
|
|
|
setIsEditingUrl(false);
|
|
|
|
|
|
|
|
setEditingUrl('');
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 保存 URL
|
|
|
|
|
|
|
|
const handleSaveUrl = async () => {
|
|
|
|
|
|
|
|
if (!editingUrl.trim()) {
|
|
|
|
|
|
|
|
setMessage({ type: 'error', text: '请输入有效的 URL' });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setSavingUrl(true);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await adminApi.updateAKShareConfig({ baseUrl: editingUrl.trim() });
|
|
|
|
|
|
|
|
setAkshareConfig(prev => ({ ...prev, baseUrl: editingUrl.trim() }));
|
|
|
|
|
|
|
|
setMessage({ type: 'success', text: 'AKShare 地址已更新,重启后端服务后生效' });
|
|
|
|
|
|
|
|
setIsEditingUrl(false);
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
|
|
setMessage({ type: 'error', text: error.message || '保存失败' });
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
setSavingUrl(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 开始编辑自定义数据源
|
|
|
|
|
|
|
|
const handleStartEditCustom = () => {
|
|
|
|
|
|
|
|
const customSource = sources.find(s => s.id === 'custom');
|
|
|
|
|
|
|
|
if (customSource) {
|
|
|
|
|
|
|
|
setCustomUrl(customSource.url);
|
|
|
|
|
|
|
|
setCustomEnabled(customSource.enabled);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setEditingCustom(true);
|
|
|
|
setEditingService(true);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 取消编辑自定义数据源
|
|
|
|
// 取消编辑统一数据服务
|
|
|
|
const handleCancelEditCustom = () => {
|
|
|
|
const handleCancelEditService = () => {
|
|
|
|
setEditingCustom(false);
|
|
|
|
setEditingService(false);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 保存自定义数据源配置
|
|
|
|
// 保存统一数据服务配置
|
|
|
|
const handleSaveCustom = async () => {
|
|
|
|
const handleSaveService = async () => {
|
|
|
|
if (!customUrl.trim()) {
|
|
|
|
if (!serviceUrl.trim()) {
|
|
|
|
setMessage({ type: 'error', text: '请输入有效的 URL' });
|
|
|
|
setMessage({ type: 'error', text: '请输入有效的 URL' });
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSavingCustom(true);
|
|
|
|
setSavingService(true);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await adminApi.updateDataSource('custom', {
|
|
|
|
const serviceSource = sources.find(s => s.id === MARKET_DATA_SERVICE_ID || s.type === 'custom');
|
|
|
|
url: customUrl.trim(),
|
|
|
|
const sourceId = serviceSource?.id || MARKET_DATA_SERVICE_ID;
|
|
|
|
enabled: customEnabled,
|
|
|
|
|
|
|
|
|
|
|
|
await adminApi.updateDataSource(sourceId, {
|
|
|
|
|
|
|
|
url: serviceUrl.trim(),
|
|
|
|
|
|
|
|
enabled: serviceEnabled,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
setMessage({
|
|
|
|
setMessage({
|
|
|
|
type: 'success',
|
|
|
|
type: 'success',
|
|
|
|
text: `自定义数据源已${customEnabled ? '启用' : '禁用'},地址: ${customUrl.trim()}`
|
|
|
|
text: `统一数据服务已${serviceEnabled ? '启用' : '禁用'},地址: ${serviceUrl.trim()}`
|
|
|
|
});
|
|
|
|
});
|
|
|
|
setEditingCustom(false);
|
|
|
|
setEditingService(false);
|
|
|
|
await fetchData();
|
|
|
|
await fetchData();
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
setMessage({ type: 'error', text: error.message || '保存失败' });
|
|
|
|
setMessage({ type: 'error', text: error.message || '保存失败' });
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
setSavingCustom(false);
|
|
|
|
setSavingService(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const akshareSource = sources.find(s => s.type === 'akshare');
|
|
|
|
const marketDataService = sources.find(s => s.id === MARKET_DATA_SERVICE_ID || s.type === 'custom');
|
|
|
|
|
|
|
|
|
|
|
|
// 加载中
|
|
|
|
// 加载中
|
|
|
|
if (loading) {
|
|
|
|
if (loading) {
|
|
|
|
@ -296,7 +211,7 @@ export default function DataSourceConfig() {
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h1 className="text-2xl font-bold text-white">数据源配置</h1>
|
|
|
|
<h1 className="text-2xl font-bold text-white">数据源配置</h1>
|
|
|
|
<p className="text-[#b0b0b0] mt-1">管理 AKShare 数据源和自定义数据源连接</p>
|
|
|
|
<p className="text-[#b0b0b0] mt-1">管理统一数据服务连接</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@ -322,7 +237,7 @@ export default function DataSourceConfig() {
|
|
|
|
</motion.div>
|
|
|
|
</motion.div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 自定义数据源配置卡片 */}
|
|
|
|
{/* 统一数据服务配置卡片 */}
|
|
|
|
<motion.div
|
|
|
|
<motion.div
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
@ -331,27 +246,27 @@ export default function DataSourceConfig() {
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<div className={`w-14 h-14 rounded-xl flex items-center justify-center ${
|
|
|
|
<div className={`w-14 h-14 rounded-xl flex items-center justify-center ${
|
|
|
|
customEnabled
|
|
|
|
serviceEnabled
|
|
|
|
? 'bg-[#ff6b35]/20'
|
|
|
|
? 'bg-[#ff6b35]/20'
|
|
|
|
: 'bg-[#2a2a2a]'
|
|
|
|
: 'bg-[#2a2a2a]'
|
|
|
|
}`}>
|
|
|
|
}`}>
|
|
|
|
<Server className={`w-7 h-7 ${
|
|
|
|
<Server className={`w-7 h-7 ${
|
|
|
|
customEnabled ? 'text-[#ff6b35]' : 'text-[#666]'
|
|
|
|
serviceEnabled ? 'text-[#ff6b35]' : 'text-[#666]'
|
|
|
|
}`} />
|
|
|
|
}`} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h2 className="text-lg font-semibold text-white">自定义数据源</h2>
|
|
|
|
<h2 className="text-lg font-semibold text-white">统一数据服务</h2>
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
customEnabled ? 'bg-[#ff6b35]' : 'bg-[#666]'
|
|
|
|
serviceEnabled ? 'bg-[#ff6b35]' : 'bg-[#666]'
|
|
|
|
}`} />
|
|
|
|
}`} />
|
|
|
|
<span className={`text-sm ${
|
|
|
|
<span className={`text-sm ${
|
|
|
|
customEnabled ? 'text-[#ff6b35]' : 'text-[#666]'
|
|
|
|
serviceEnabled ? 'text-[#ff6b35]' : 'text-[#666]'
|
|
|
|
}`}>
|
|
|
|
}`}>
|
|
|
|
{customEnabled ? '已启用' : '未启用'}
|
|
|
|
{serviceEnabled ? '已启用' : '未启用'}
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
{customEnabled && (
|
|
|
|
{serviceEnabled && (
|
|
|
|
<span className="text-xs text-green-400">(优先使用)</span>
|
|
|
|
<span className="text-xs text-green-400">(当前使用)</span>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@ -360,14 +275,14 @@ export default function DataSourceConfig() {
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
{/* Enable/Disable Switch */}
|
|
|
|
{/* Enable/Disable Switch */}
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={() => handleToggleSource('custom')}
|
|
|
|
onClick={() => handleToggleSource(marketDataService?.id || MARKET_DATA_SERVICE_ID)}
|
|
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
|
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
|
|
|
customEnabled
|
|
|
|
serviceEnabled
|
|
|
|
? 'bg-[#ff6b35]/20 text-[#ff6b35] hover:bg-[#ff6b35]/30'
|
|
|
|
? 'bg-[#ff6b35]/20 text-[#ff6b35] hover:bg-[#ff6b35]/30'
|
|
|
|
: 'bg-[#2a2a2a] text-[#666] hover:bg-[#333]'
|
|
|
|
: 'bg-[#2a2a2a] text-[#666] hover:bg-[#333]'
|
|
|
|
}`}
|
|
|
|
}`}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{customEnabled ? (
|
|
|
|
{serviceEnabled ? (
|
|
|
|
<>
|
|
|
|
<>
|
|
|
|
<Pause className="w-4 h-4" />
|
|
|
|
<Pause className="w-4 h-4" />
|
|
|
|
<span>已启用</span>
|
|
|
|
<span>已启用</span>
|
|
|
|
@ -382,11 +297,11 @@ export default function DataSourceConfig() {
|
|
|
|
|
|
|
|
|
|
|
|
{/* Test Button */}
|
|
|
|
{/* Test Button */}
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={() => handleTestConnection('custom')}
|
|
|
|
onClick={() => handleTestConnection(marketDataService?.id || MARKET_DATA_SERVICE_ID)}
|
|
|
|
disabled={testing === 'custom'}
|
|
|
|
disabled={testing === (marketDataService?.id || MARKET_DATA_SERVICE_ID)}
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{testing === 'custom' ? (
|
|
|
|
{testing === (marketDataService?.id || MARKET_DATA_SERVICE_ID) ? (
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<Zap className="w-4 h-4" />
|
|
|
|
<Zap className="w-4 h-4" />
|
|
|
|
@ -395,9 +310,9 @@ export default function DataSourceConfig() {
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Edit Button */}
|
|
|
|
{/* Edit Button */}
|
|
|
|
{!editingCustom ? (
|
|
|
|
{!editingService ? (
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={handleStartEditCustom}
|
|
|
|
onClick={handleStartEditService}
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<Settings className="w-4 h-4" />
|
|
|
|
<Settings className="w-4 h-4" />
|
|
|
|
@ -405,7 +320,7 @@ export default function DataSourceConfig() {
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={handleCancelEditCustom}
|
|
|
|
onClick={handleCancelEditService}
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
@ -415,8 +330,8 @@ export default function DataSourceConfig() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Custom Data Source Config */}
|
|
|
|
{/* Market Data Service Config */}
|
|
|
|
{editingCustom && (
|
|
|
|
{editingService && (
|
|
|
|
<motion.div
|
|
|
|
<motion.div
|
|
|
|
initial={{ opacity: 0, height: 0 }}
|
|
|
|
initial={{ opacity: 0, height: 0 }}
|
|
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
|
|
@ -426,13 +341,13 @@ export default function DataSourceConfig() {
|
|
|
|
<label className="text-sm text-[#b0b0b0]">服务地址</label>
|
|
|
|
<label className="text-sm text-[#b0b0b0]">服务地址</label>
|
|
|
|
<input
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
type="text"
|
|
|
|
value={customUrl}
|
|
|
|
value={serviceUrl}
|
|
|
|
onChange={(e) => setCustomUrl(e.target.value)}
|
|
|
|
onChange={(e) => setServiceUrl(e.target.value)}
|
|
|
|
placeholder="http://localhost:8000"
|
|
|
|
placeholder="http://localhost:8080"
|
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white outline-none focus:border-[#ff6b35]"
|
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white outline-none focus:border-[#ff6b35]"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
<p className="text-xs text-[#666]">
|
|
|
|
<p className="text-xs text-[#666]">
|
|
|
|
输入Python数据服务的地址,例如: http://localhost:8000 或 http://192.168.1.100:8000
|
|
|
|
输入统一数据服务地址,例如: http://localhost:8080 或 http://192.168.1.100:8080
|
|
|
|
</p>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@ -440,27 +355,27 @@ export default function DataSourceConfig() {
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
<input
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
type="checkbox"
|
|
|
|
checked={customEnabled}
|
|
|
|
checked={serviceEnabled}
|
|
|
|
onChange={(e) => setCustomEnabled(e.target.checked)}
|
|
|
|
onChange={(e) => setServiceEnabled(e.target.checked)}
|
|
|
|
className="w-4 h-4 rounded border-[#2a2a2a] bg-[#0a0a0a] text-[#ff6b35] focus:ring-[#ff6b35]"
|
|
|
|
className="w-4 h-4 rounded border-[#2a2a2a] bg-[#0a0a0a] text-[#ff6b35] focus:ring-[#ff6b35]"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
<span className="text-sm text-white">启用此数据源</span>
|
|
|
|
<span className="text-sm text-white">启用此服务</span>
|
|
|
|
</label>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={handleCancelEditCustom}
|
|
|
|
onClick={handleCancelEditService}
|
|
|
|
className="px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
className="px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
取消
|
|
|
|
取消
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={handleSaveCustom}
|
|
|
|
onClick={handleSaveService}
|
|
|
|
disabled={savingCustom}
|
|
|
|
disabled={savingService}
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{savingCustom ? (
|
|
|
|
{savingService ? (
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<Save className="w-4 h-4" />
|
|
|
|
<Save className="w-4 h-4" />
|
|
|
|
@ -472,179 +387,14 @@ export default function DataSourceConfig() {
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Current Config Display */}
|
|
|
|
{/* Current Config Display */}
|
|
|
|
{!editingCustom && (
|
|
|
|
{!editingService && (
|
|
|
|
<div className="mt-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
|
<div className="mt-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<Globe className="w-4 h-4 text-[#666]" />
|
|
|
|
<Globe className="w-4 h-4 text-[#666]" />
|
|
|
|
<span className="text-sm text-[#b0b0b0]">当前地址</span>
|
|
|
|
<span className="text-sm text-[#b0b0b0]">当前地址</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<span className="text-sm text-white font-mono">{customUrl}</span>
|
|
|
|
<span className="text-sm text-white font-mono">{serviceUrl}</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* AKShare Status Card */}
|
|
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
|
|
transition={{ delay: 0.1 }}
|
|
|
|
|
|
|
|
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-6"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
|
|
<div className={`w-14 h-14 rounded-xl flex items-center justify-center ${
|
|
|
|
|
|
|
|
akshareStatus?.connected
|
|
|
|
|
|
|
|
? 'bg-green-500/20'
|
|
|
|
|
|
|
|
: 'bg-red-500/20'
|
|
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
|
|
<Database className={`w-7 h-7 ${
|
|
|
|
|
|
|
|
akshareStatus?.connected ? 'text-green-400' : 'text-red-400'
|
|
|
|
|
|
|
|
}`} />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
|
|
<h2 className="text-lg font-semibold text-white">AKShare 数据源</h2>
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
|
|
|
|
akshareStatus?.connected ? 'bg-green-500' : 'bg-red-500'
|
|
|
|
|
|
|
|
}`} />
|
|
|
|
|
|
|
|
<span className={`text-sm ${
|
|
|
|
|
|
|
|
akshareStatus?.connected ? 'text-green-400' : 'text-red-400'
|
|
|
|
|
|
|
|
}`}>
|
|
|
|
|
|
|
|
{akshareStatus?.connected ? '连接正常' : '未连接'}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
{!customEnabled && akshareStatus?.connected && (
|
|
|
|
|
|
|
|
<span className="text-xs text-green-400">(当前使用)</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{akshareStatus?.version && (
|
|
|
|
|
|
|
|
<span className="text-sm text-[#666]">版本: {akshareStatus.version}</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
|
|
{/* Enable/Disable Switch */}
|
|
|
|
|
|
|
|
{akshareSource && (
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={() => handleToggleSource(akshareSource.id)}
|
|
|
|
|
|
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
|
|
|
|
|
|
|
akshareSource.enabled
|
|
|
|
|
|
|
|
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
|
|
|
|
|
|
|
|
: 'bg-[#2a2a2a] text-[#666] hover:bg-[#333]'
|
|
|
|
|
|
|
|
}`}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{akshareSource.enabled ? (
|
|
|
|
|
|
|
|
<>
|
|
|
|
|
|
|
|
<Pause className="w-4 h-4" />
|
|
|
|
|
|
|
|
<span>已启用</span>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<>
|
|
|
|
|
|
|
|
<Play className="w-4 h-4" />
|
|
|
|
|
|
|
|
<span>已禁用</span>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Test Button */}
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={() => handleTestConnection('akshare')}
|
|
|
|
|
|
|
|
disabled={testing === 'akshare'}
|
|
|
|
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{testing === 'akshare' ? (
|
|
|
|
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<Zap className="w-4 h-4" />
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
测试连接
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sync Button */}
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={handleManualSync}
|
|
|
|
|
|
|
|
disabled={syncing || !akshareSource?.enabled}
|
|
|
|
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors disabled:opacity-50"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{syncing ? (
|
|
|
|
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<RefreshCw className="w-4 h-4" />
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
立即同步
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* AKShare URL Config */}
|
|
|
|
|
|
|
|
<div className="mt-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
|
|
<Globe className="w-4 h-4 text-[#666]" />
|
|
|
|
|
|
|
|
<span className="text-sm text-[#b0b0b0]">服务地址</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{!isEditingUrl ? (
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
|
|
<span className="text-sm text-white font-mono">{akshareConfig.baseUrl}</span>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={handleStartEditUrl}
|
|
|
|
|
|
|
|
className="p-1.5 hover:bg-[#2a2a2a] rounded transition-colors"
|
|
|
|
|
|
|
|
title="修改地址"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<Edit2 className="w-4 h-4 text-[#666]" />
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
|
|
value={editingUrl}
|
|
|
|
|
|
|
|
onChange={(e) => setEditingUrl(e.target.value)}
|
|
|
|
|
|
|
|
placeholder="http://localhost:8000"
|
|
|
|
|
|
|
|
className="bg-[#0a0a0a] border border-[#2a2a2a] rounded px-3 py-1.5 text-sm text-white w-64 outline-none focus:border-[#ff6b35]"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={handleSaveUrl}
|
|
|
|
|
|
|
|
disabled={savingUrl}
|
|
|
|
|
|
|
|
className="p-1.5 bg-green-500/20 text-green-400 rounded hover:bg-green-500/30 transition-colors disabled:opacity-50"
|
|
|
|
|
|
|
|
title="保存"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{savingUrl ? (
|
|
|
|
|
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
|
|
) : (
|
|
|
|
|
|
|
|
<Save className="w-4 h-4" />
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={handleCancelEditUrl}
|
|
|
|
|
|
|
|
className="p-1.5 hover:bg-[#2a2a2a] rounded transition-colors"
|
|
|
|
|
|
|
|
title="取消"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<X className="w-4 h-4 text-[#666]" />
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p className="text-xs text-[#666] mt-2">
|
|
|
|
|
|
|
|
{customEnabled
|
|
|
|
|
|
|
|
? '自定义数据源已启用,AKShare将作为备用数据源'
|
|
|
|
|
|
|
|
: '修改后需要重启后端服务才能生效。Docker 环境可使用 http://host.docker.internal:8000'}
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Supported APIs */}
|
|
|
|
|
|
|
|
{akshareStatus?.supportedApis && akshareStatus.supportedApis.length > 0 && (
|
|
|
|
|
|
|
|
<div className="mt-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
|
|
|
|
|
<h3 className="text-sm text-[#b0b0b0] mb-2">可用 API</h3>
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
{akshareStatus.supportedApis.map(api => (
|
|
|
|
|
|
|
|
<span key={api} className="px-3 py-1 bg-[#0a0a0a] rounded-full text-xs text-[#666]">
|
|
|
|
|
|
|
|
{api}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
@ -654,7 +404,7 @@ export default function DataSourceConfig() {
|
|
|
|
<motion.div
|
|
|
|
<motion.div
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
transition={{ delay: 0.1 }}
|
|
|
|
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-6"
|
|
|
|
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-6"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<h2 className="text-lg font-semibold text-white mb-4">数据源列表</h2>
|
|
|
|
<h2 className="text-lg font-semibold text-white mb-4">数据源列表</h2>
|
|
|
|
@ -664,29 +414,20 @@ export default function DataSourceConfig() {
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
key={source.id}
|
|
|
|
key={source.id}
|
|
|
|
className={`flex items-center justify-between p-4 bg-[#0a0a0a] rounded-lg border ${
|
|
|
|
className={`flex items-center justify-between p-4 bg-[#0a0a0a] rounded-lg border ${
|
|
|
|
source.enabled && source.id === 'custom'
|
|
|
|
source.enabled
|
|
|
|
? 'border-[#ff6b35]/50'
|
|
|
|
? 'border-[#ff6b35]/50'
|
|
|
|
: source.enabled
|
|
|
|
|
|
|
|
? 'border-green-500/30'
|
|
|
|
|
|
|
|
: 'border-transparent'
|
|
|
|
: 'border-transparent'
|
|
|
|
}`}
|
|
|
|
}`}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
|
|
source.enabled
|
|
|
|
source.enabled ? 'bg-[#ff6b35]' : 'bg-[#666]'
|
|
|
|
? source.id === 'custom' ? 'bg-[#ff6b35]' : 'bg-green-500'
|
|
|
|
|
|
|
|
: 'bg-[#666]'
|
|
|
|
|
|
|
|
}`} />
|
|
|
|
}`} />
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<span className="text-white font-medium">{source.name}</span>
|
|
|
|
<span className="text-white font-medium">{source.name}</span>
|
|
|
|
{source.enabled && source.id === 'custom' && (
|
|
|
|
{source.enabled && (
|
|
|
|
<span className="px-2 py-0.5 bg-[#ff6b35]/20 text-[#ff6b35] text-xs rounded">
|
|
|
|
<span className="px-2 py-0.5 bg-[#ff6b35]/20 text-[#ff6b35] text-xs rounded">
|
|
|
|
优先
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{source.enabled && source.id === 'akshare' && !customEnabled && (
|
|
|
|
|
|
|
|
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded">
|
|
|
|
|
|
|
|
当前使用
|
|
|
|
当前使用
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
@ -718,7 +459,7 @@ export default function DataSourceConfig() {
|
|
|
|
<motion.div
|
|
|
|
<motion.div
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
transition={{ delay: 0.3 }}
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
className="bg-blue-500/10 border border-blue-500/30 rounded-xl p-5"
|
|
|
|
className="bg-blue-500/10 border border-blue-500/30 rounded-xl p-5"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
@ -727,10 +468,9 @@ export default function DataSourceConfig() {
|
|
|
|
<h3 className="text-blue-400 font-medium">数据源说明</h3>
|
|
|
|
<h3 className="text-blue-400 font-medium">数据源说明</h3>
|
|
|
|
<ul className="text-sm text-blue-300/80 mt-2 space-y-1 list-disc list-inside">
|
|
|
|
<ul className="text-sm text-blue-300/80 mt-2 space-y-1 list-disc list-inside">
|
|
|
|
<li><strong>自定义数据源</strong>: 可配置为本地或远程 Python 数据服务,支持 IP 地址配置</li>
|
|
|
|
<li><strong>自定义数据源</strong>: 可配置为本地或远程 Python 数据服务,支持 IP 地址配置</li>
|
|
|
|
<li><strong>AKShare 官方</strong>: 开源财经数据接口库,无需 API Key</li>
|
|
|
|
<li>默认地址为 http://localhost:8080,可根据实际情况修改</li>
|
|
|
|
<li>启用自定义数据源后,系统将优先使用自定义数据源获取数据</li>
|
|
|
|
|
|
|
|
<li>自定义数据源不可用时,自动回退到 AKShare 数据源</li>
|
|
|
|
|
|
|
|
<li>点击"测试连接"验证数据源是否可用</li>
|
|
|
|
<li>点击"测试连接"验证数据源是否可用</li>
|
|
|
|
|
|
|
|
<li>修改配置后需要重启后端服务才能生效</li>
|
|
|
|
</ul>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|