mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-06 18:22:44 +08:00
236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
import { Alert, message, Tabs, Space } from 'antd';
|
|
import { useEffect, useState } from 'react';
|
|
import PageCard from '../../components/PageCard';
|
|
import { getAllConfig, setConfig } from '../../api/config';
|
|
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined, MailOutlined, CloudSyncOutlined } from '@ant-design/icons';
|
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
import '../../styles/settings-tabs.css';
|
|
import { useI18n } from '../../i18n';
|
|
import useResponsive from '../../hooks/useResponsive';
|
|
import AppearanceSettingsTab from './components/AppearanceSettingsTab';
|
|
import AppSettingsTab from './components/AppSettingsTab';
|
|
import AiSettingsTab from './components/AiSettingsTab';
|
|
import VectorDbSettingsTab from './components/VectorDbSettingsTab';
|
|
import EmailSettingsTab from './components/EmailSettingsTab';
|
|
import ProtocolMappingsTab from './components/ProtocolMappingsTab';
|
|
|
|
type TabKey = 'appearance' | 'app' | 'email' | 'ai' | 'vector-db' | 'mappings';
|
|
|
|
const TAB_KEYS: TabKey[] = ['appearance', 'app', 'email', 'ai', 'vector-db', 'mappings'];
|
|
const DEFAULT_TAB: TabKey = 'appearance';
|
|
|
|
const isValidTab = (key?: string): key is TabKey => !!key && (TAB_KEYS as string[]).includes(key);
|
|
|
|
interface SystemSettingsPageProps {
|
|
tabKey?: string;
|
|
onTabNavigate?: (key: TabKey, options?: { replace?: boolean }) => void;
|
|
}
|
|
|
|
const APP_CONFIG_KEYS: { key: string, label: string, default?: string }[] = [
|
|
{ key: 'APP_NAME', label: 'App Name' },
|
|
{ key: 'APP_LOGO', label: 'Logo URL' },
|
|
{ key: 'APP_FAVICON', label: 'Favicon URL', default: '/logo.svg' },
|
|
{ key: 'APP_DOMAIN', label: 'App Domain' },
|
|
{ key: 'FILE_DOMAIN', label: 'File Domain' },
|
|
];
|
|
|
|
// Theme related config keys
|
|
const THEME_KEYS = {
|
|
MODE: 'THEME_MODE',
|
|
PRIMARY: 'THEME_PRIMARY_COLOR',
|
|
RADIUS: 'THEME_BORDER_RADIUS',
|
|
TOKENS: 'THEME_CUSTOM_TOKENS',
|
|
CSS: 'THEME_CUSTOM_CSS',
|
|
};
|
|
|
|
export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSettingsPageProps) {
|
|
const [loading, setLoading] = useState(false);
|
|
const [config, setConfigState] = useState<Record<string, string> | null>(null);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState<TabKey>(() =>
|
|
isValidTab(tabKey) ? tabKey : DEFAULT_TAB
|
|
);
|
|
const { refreshTheme } = useTheme();
|
|
const { t } = useI18n();
|
|
const { isMobile } = useResponsive();
|
|
|
|
useEffect(() => {
|
|
getAllConfig()
|
|
.then((data) => {
|
|
setLoadError(null);
|
|
setConfigState(data as Record<string, string>);
|
|
})
|
|
.catch((e: any) => {
|
|
setLoadError(e?.message || t('Load failed'));
|
|
setConfigState({});
|
|
});
|
|
}, [t]);
|
|
|
|
const handleSave = async (values: Record<string, unknown>) => {
|
|
setLoading(true);
|
|
try {
|
|
for (const [key, value] of Object.entries(values)) {
|
|
await setConfig(key, String(value ?? ''));
|
|
}
|
|
message.success(t('Saved successfully'));
|
|
const stringValues = Object.fromEntries(
|
|
Object.entries(values).map(([key, value]) => [key, String(value ?? '')]),
|
|
) as Record<string, string>;
|
|
setConfigState((prev) => ({ ...(prev ?? {}), ...stringValues }));
|
|
// trigger theme refresh if related keys changed
|
|
if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) {
|
|
await refreshTheme();
|
|
}
|
|
} catch (e: any) {
|
|
message.error(e.message || t('Save failed'));
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览)
|
|
useEffect(() => {
|
|
if (!isValidTab(tabKey)) {
|
|
setActiveTab((prev) => (prev === DEFAULT_TAB ? prev : DEFAULT_TAB));
|
|
if (tabKey !== DEFAULT_TAB) {
|
|
onTabNavigate?.(DEFAULT_TAB, { replace: true });
|
|
}
|
|
return;
|
|
}
|
|
setActiveTab((prev) => (prev === tabKey ? prev : tabKey));
|
|
}, [tabKey, onTabNavigate]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab !== 'appearance') {
|
|
refreshTheme();
|
|
}
|
|
}, [activeTab, refreshTheme]);
|
|
|
|
const handleTabChange = (key: string) => {
|
|
const nextKey: TabKey = isValidTab(key) ? key : DEFAULT_TAB;
|
|
if (nextKey !== activeTab) {
|
|
setActiveTab(nextKey);
|
|
}
|
|
onTabNavigate?.(nextKey);
|
|
};
|
|
|
|
if (loadError) {
|
|
return (
|
|
<PageCard title={t('System Settings')}>
|
|
<Alert type="error" showIcon message={loadError} />
|
|
</PageCard>
|
|
);
|
|
}
|
|
|
|
if (!config) {
|
|
return <PageCard title={t('System Settings')}><div>{t('Loading...')}</div></PageCard>;
|
|
}
|
|
|
|
return (
|
|
<PageCard
|
|
title={t('System Settings')}
|
|
>
|
|
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
|
<Tabs
|
|
className="fx-settings-tabs"
|
|
activeKey={activeTab}
|
|
onChange={handleTabChange}
|
|
centered={!isMobile}
|
|
items={[
|
|
{
|
|
key: 'appearance',
|
|
label: (
|
|
<span>
|
|
<SkinOutlined style={{ marginRight: 8 }} />
|
|
{t('Appearance Settings')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<AppearanceSettingsTab
|
|
config={config}
|
|
loading={loading}
|
|
onSave={handleSave}
|
|
themeKeys={THEME_KEYS}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
key: 'app',
|
|
label: (
|
|
<span>
|
|
<AppstoreOutlined style={{ marginRight: 8 }} />
|
|
{t('App Settings')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<AppSettingsTab
|
|
config={config}
|
|
loading={loading}
|
|
onSave={handleSave}
|
|
configKeys={APP_CONFIG_KEYS}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: 'email',
|
|
label: (
|
|
<span>
|
|
<MailOutlined style={{ marginRight: 8 }} />
|
|
{t('Email Settings')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<EmailSettingsTab
|
|
config={config}
|
|
loading={loading}
|
|
onSave={handleSave}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: 'ai',
|
|
label: (
|
|
<span>
|
|
<RobotOutlined style={{ marginRight: 8 }} />
|
|
{t('AI Settings')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<AiSettingsTab
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: 'vector-db',
|
|
label: (
|
|
<span>
|
|
<DatabaseOutlined style={{ marginRight: 8 }} />
|
|
{t('Vector Database')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<VectorDbSettingsTab isActive={activeTab === 'vector-db'} />
|
|
),
|
|
},
|
|
{
|
|
key: 'mappings',
|
|
label: (
|
|
<span>
|
|
<CloudSyncOutlined style={{ marginRight: 8 }} />
|
|
{t('Protocol Mappings')}
|
|
</span>
|
|
),
|
|
children: (
|
|
<ProtocolMappingsTab
|
|
config={config}
|
|
loading={loading}
|
|
onSave={handleSave}
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Space>
|
|
</PageCard>
|
|
);
|
|
}
|