mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-02 22:20:01 +08:00
feat: add i18n with language switcher and English/Chinese translations
This commit is contained in:
@@ -3,19 +3,21 @@ import { Button, Typography, Upload, message, Modal } from 'antd';
|
||||
import PageCard from '../../components/PageCard';
|
||||
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { backupApi } from '../../api/backup';
|
||||
import { useI18n } from '../../i18n';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
const BackupPage = memo(function BackupPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleExport = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await backupApi.export();
|
||||
message.success('导出已开始,请检查您的下载。');
|
||||
message.success(t('Export started, check your downloads.'));
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '导出失败');
|
||||
message.error(e.message || t('Export failed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -23,24 +25,24 @@ const BackupPage = memo(function BackupPage() {
|
||||
|
||||
const handleImport = (file: File) => {
|
||||
Modal.confirm({
|
||||
title: '确认导入备份?',
|
||||
title: t('Confirm import backup?'),
|
||||
content: (
|
||||
<Typography>
|
||||
<Paragraph>您确定要从此文件导入数据吗?</Paragraph>
|
||||
<Paragraph strong>警告:此操作将覆盖当前数据库中的所有现有数据,包括用户(含密码)、设置、存储和任务。此操作不可逆!</Paragraph>
|
||||
<Paragraph>{t('Are you sure to import from this file?')}</Paragraph>
|
||||
<Paragraph strong>{t('Warning: This will overwrite all data including users (with passwords), settings, storages and tasks. Irreversible!')}</Paragraph>
|
||||
</Typography>
|
||||
),
|
||||
okText: '确认导入',
|
||||
okText: t('Confirm Import'),
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
cancelText: t('Cancel'),
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await backupApi.import(file);
|
||||
message.success(response.message || '导入成功!页面将刷新。');
|
||||
message.success(response.message || t('Import succeeded! The page will refresh.'));
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '导入失败');
|
||||
message.error(e.message || t('Import failed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -50,33 +52,33 @@ const BackupPage = memo(function BackupPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageCard title="备份和恢复">
|
||||
<PageCard title={t('Backup & Restore')}>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<PageCard title="导出" style={{ flex: 1 }}>
|
||||
<PageCard title={t('Export')} style={{ flex: 1 }}>
|
||||
<Paragraph>
|
||||
点击下面的按钮将所有数据(包括存储、用户、自动化任务和分享)导出为一个 JSON 文件。
|
||||
<Text strong>请妥善保管您的备份文件。</Text>
|
||||
{t('Export all data (adapters, users, tasks, shares) into a JSON file.')}
|
||||
<Text strong>{t('Keep your backup file safe.')}</Text>
|
||||
</Paragraph>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
loading={loading}
|
||||
>
|
||||
导出备份
|
||||
{t('Export Backup')}
|
||||
</Button>
|
||||
</PageCard>
|
||||
<PageCard title="恢复" style={{ flex: 1 }}>
|
||||
<PageCard title={t('Import')} style={{ flex: 1 }}>
|
||||
<Paragraph>
|
||||
从之前导出的JSON文件恢复数据。
|
||||
<Text strong type="danger">警告:此操作将清除并覆盖现有数据。</Text>
|
||||
{t('Restore data from a previously exported JSON file.')}
|
||||
<Text strong type="danger">{t('Warning: This will clear and overwrite existing data.')}</Text>
|
||||
</Paragraph>
|
||||
<Upload
|
||||
beforeUpload={handleImport}
|
||||
showUploadList={false}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={loading}>
|
||||
选择文件并恢复
|
||||
{t('Choose File and Restore')}
|
||||
</Button>
|
||||
</Upload>
|
||||
</PageCard>
|
||||
@@ -85,4 +87,4 @@ const BackupPage = memo(function BackupPage() {
|
||||
);
|
||||
});
|
||||
|
||||
export default BackupPage;
|
||||
export default BackupPage;
|
||||
|
||||
@@ -6,24 +6,25 @@ import { vectorDBApi } from '../../api/vectorDB';
|
||||
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import '../../styles/settings-tabs.css';
|
||||
import { useI18n } from '../../i18n';
|
||||
|
||||
const APP_CONFIG_KEYS: {key: string, label: string, default?: string}[] = [
|
||||
{ key: 'APP_NAME', label: '应用名称' },
|
||||
{ key: 'APP_LOGO', label: 'LOGO地址' },
|
||||
{ key: 'APP_DOMAIN', label: '应用域名' },
|
||||
{ key: 'FILE_DOMAIN', label: '文件域名' },
|
||||
{ key: 'APP_NAME', label: 'App Name' },
|
||||
{ key: 'APP_LOGO', label: 'Logo URL' },
|
||||
{ key: 'APP_DOMAIN', label: 'App Domain' },
|
||||
{ key: 'FILE_DOMAIN', label: 'File Domain' },
|
||||
];
|
||||
|
||||
const VISION_CONFIG_KEYS = [
|
||||
{ key: 'AI_VISION_API_URL', label: '视觉模型 API 地址' },
|
||||
{ key: 'AI_VISION_MODEL', label: '视觉模型', default: 'Qwen/Qwen2.5-VL-32B-Instruct' },
|
||||
{ key: 'AI_VISION_API_KEY', label: '视觉模型 API Key' },
|
||||
{ key: 'AI_VISION_API_URL', label: 'Vision API URL' },
|
||||
{ key: 'AI_VISION_MODEL', label: 'Vision Model', default: 'Qwen/Qwen2.5-VL-32B-Instruct' },
|
||||
{ key: 'AI_VISION_API_KEY', label: 'Vision API Key' },
|
||||
];
|
||||
|
||||
const EMBED_CONFIG_KEYS = [
|
||||
{ key: 'AI_EMBED_API_URL', label: '嵌入模型 API 地址' },
|
||||
{ key: 'AI_EMBED_MODEL', label: '嵌入模型', default: 'Qwen/Qwen3-Embedding-8B' },
|
||||
{ key: 'AI_EMBED_API_KEY', label: '嵌入模型 API Key' },
|
||||
{ key: 'AI_EMBED_API_URL', label: 'Embedding API URL' },
|
||||
{ key: 'AI_EMBED_MODEL', label: 'Embedding Model', default: 'Qwen/Qwen3-Embedding-8B' },
|
||||
{ key: 'AI_EMBED_API_KEY', label: 'Embedding API Key' },
|
||||
];
|
||||
|
||||
const ALL_AI_KEYS = [...VISION_CONFIG_KEYS, ...EMBED_CONFIG_KEYS];
|
||||
@@ -42,6 +43,7 @@ export default function SystemSettingsPage() {
|
||||
const [config, setConfigState] = useState<Record<string, string> | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('appearance');
|
||||
const { refreshTheme, previewTheme } = useTheme();
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
getAllConfig().then((data) => setConfigState(data as Record<string, string>));
|
||||
@@ -53,14 +55,14 @@ export default function SystemSettingsPage() {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
await setConfig(key, String(value ?? ''));
|
||||
}
|
||||
message.success('保存成功');
|
||||
message.success(t('Saved successfully'));
|
||||
setConfigState({ ...config, ...values });
|
||||
// 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 || '保存失败');
|
||||
message.error(e.message || t('Save failed'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -73,12 +75,12 @@ export default function SystemSettingsPage() {
|
||||
}, [activeTab]);
|
||||
|
||||
if (!config) {
|
||||
return <PageCard title='系统设置'><div>加载中...</div></PageCard>;
|
||||
return <PageCard title={t('System Settings')}><div>{t('Loading...')}</div></PageCard>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageCard
|
||||
title='系统设置'
|
||||
title={t('System Settings')}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={32}>
|
||||
<Tabs
|
||||
@@ -93,7 +95,7 @@ export default function SystemSettingsPage() {
|
||||
label: (
|
||||
<span>
|
||||
<SkinOutlined style={{ marginRight: 8 }} />
|
||||
外观设置
|
||||
{t('Appearance Settings')}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
@@ -130,39 +132,39 @@ export default function SystemSettingsPage() {
|
||||
// Validate JSON if provided
|
||||
if (vals[THEME_KEYS.TOKENS]) {
|
||||
try { JSON.parse(vals[THEME_KEYS.TOKENS]); }
|
||||
catch { return message.error('高级 Token 需为合法 JSON'); }
|
||||
catch { return message.error(t('Advanced tokens must be valid JSON')); }
|
||||
}
|
||||
await handleSave(vals);
|
||||
}}
|
||||
style={{ marginTop: 24 }}
|
||||
key={'appearance-' + JSON.stringify(config)}
|
||||
>
|
||||
<Card title="主题">
|
||||
<Form.Item name={THEME_KEYS.MODE} label="主题模式">
|
||||
<Card title={t('Theme')}>
|
||||
<Form.Item name={THEME_KEYS.MODE} label={t('Theme Mode')}>
|
||||
<Radio.Group buttonStyle="solid">
|
||||
<Radio.Button value="light">亮色</Radio.Button>
|
||||
<Radio.Button value="dark">暗色</Radio.Button>
|
||||
<Radio.Button value="system">跟随系统</Radio.Button>
|
||||
<Radio.Button value="light">{t('Light')}</Radio.Button>
|
||||
<Radio.Button value="dark">{t('Dark')}</Radio.Button>
|
||||
<Radio.Button value="system">{t('Follow System')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name={THEME_KEYS.PRIMARY} label="主色">
|
||||
<Form.Item name={THEME_KEYS.PRIMARY} label={t('Primary Color')}>
|
||||
<Input type="color" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name={THEME_KEYS.RADIUS} label="圆角">
|
||||
<Form.Item name={THEME_KEYS.RADIUS} label={t('Border Radius')}>
|
||||
<InputNumber min={0} max={24} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Card title="高级" style={{ marginTop: 24 }}>
|
||||
<Form.Item name={THEME_KEYS.TOKENS} label="覆盖 AntD Token(JSON)" tooltip="例如:{ "colorText": "#222" }">
|
||||
<Card title={t('Advanced')} style={{ marginTop: 24 }}>
|
||||
<Form.Item name={THEME_KEYS.TOKENS} label={t('Override AntD Tokens (JSON)')} tooltip={t('e.g. {"colorText": "#222"}') }>
|
||||
<Input.TextArea autoSize={{ minRows: 4 }} placeholder='{ "colorText": "#222" }' />
|
||||
</Form.Item>
|
||||
<Form.Item name={THEME_KEYS.CSS} label="自定义 CSS">
|
||||
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* 支持任意 CSS */"} />
|
||||
<Form.Item name={THEME_KEYS.CSS} label={t('Custom CSS')}>
|
||||
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* CSS */"} />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ marginTop: 24 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
保存
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -173,7 +175,7 @@ export default function SystemSettingsPage() {
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined style={{ marginRight: 8 }} />
|
||||
应用设置
|
||||
{t('App Settings')}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
@@ -187,13 +189,13 @@ export default function SystemSettingsPage() {
|
||||
key={JSON.stringify(config)}
|
||||
>
|
||||
{APP_CONFIG_KEYS.map(({ key, label }) => (
|
||||
<Form.Item key={key} name={key} label={label}>
|
||||
<Form.Item key={key} name={key} label={t(label)}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
保存
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -204,7 +206,7 @@ export default function SystemSettingsPage() {
|
||||
label: (
|
||||
<span>
|
||||
<RobotOutlined style={{ marginRight: 8 }} />
|
||||
AI设置
|
||||
{t('AI Settings')}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
@@ -217,23 +219,23 @@ export default function SystemSettingsPage() {
|
||||
style={{ marginTop: 24 }}
|
||||
key={JSON.stringify(config)}
|
||||
>
|
||||
<Card title="视觉模型" style={{ marginBottom: 24 }}>
|
||||
<Card title={t('Vision Model')} style={{ marginBottom: 24 }}>
|
||||
{VISION_CONFIG_KEYS.map(({ key, label }) => (
|
||||
<Form.Item key={key} name={key} label={label}>
|
||||
<Form.Item key={key} name={key} label={t(label)}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
))}
|
||||
</Card>
|
||||
<Card title="嵌入模型">
|
||||
<Card title={t('Embedding Model')}>
|
||||
{EMBED_CONFIG_KEYS.map(({ key, label }) => (
|
||||
<Form.Item key={key} name={key} label={label}>
|
||||
<Form.Item key={key} name={key} label={t(label)}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
))}
|
||||
</Card>
|
||||
<Form.Item style={{ marginTop: 24 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
保存
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -244,16 +246,16 @@ export default function SystemSettingsPage() {
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
向量数据库
|
||||
{t('Vector Database')}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Card title="向量数据库设置" style={{ marginTop: 24 }}>
|
||||
<Card title={t('Vector Database Settings')} style={{ marginTop: 24 }}>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="数据库类型">
|
||||
<Form.Item label={t('Database Type')}>
|
||||
<Select
|
||||
size="large"
|
||||
value="Milvus Lite"
|
||||
value={'Milvus Lite'}
|
||||
disabled
|
||||
options={[{ value: 'Milvus Lite', label: 'Milvus Lite' }]}
|
||||
/>
|
||||
@@ -264,23 +266,23 @@ export default function SystemSettingsPage() {
|
||||
block
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: '确认清空向量数据库?',
|
||||
content: '此操作将删除所有集合中的所有数据,且不可逆。',
|
||||
okText: '确认清空',
|
||||
title: t('Confirm clear vector database?'),
|
||||
content: t('This will delete all collections irreversibly.'),
|
||||
okText: t('Confirm Clear'),
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
cancelText: t('Cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await vectorDBApi.clearAll();
|
||||
message.success('向量数据库已清空');
|
||||
message.success(t('Vector database cleared'));
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '清空失败');
|
||||
message.error(e.message || t('Clear failed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
清空向量库
|
||||
{t('Clear Vector DB')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
Reference in New Issue
Block a user